你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式
(我已經安裝了使用者樣式管理器,讓我安裝!)
// ==UserScript==
// @name T3Chat Table Copier
// @namespace wearifulpoet.com
// @version 0.1.1
// @description Add "Copy Table" buttons to tables in T3Chat to Office tools
// @match https://t3.chat/*
// @run-at document-idle
// @grant none
// @license MIT
// ==/UserScript==
(() => {
const TABLE_SELECTOR = 'table';
const CONTENT_SELECTOR = '[role="article"], .prose, [data-testid="message-content"]';
const processedTables = new WeakSet();
const createCopyButton = () => {
const button = document.createElement('button');
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
</svg>
Copy Table
`;
button.className = `
inline-flex items-center gap-2 px-3 py-1.5
text-xs font-medium text-muted-foreground
bg-muted hover:bg-muted/80
border border-border rounded-md
transition-colors duration-200
focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2
`.replace(/\s+/g, ' ').trim();
button.setAttribute('data-table-copy-button', '1');
button.setAttribute('aria-label', 'Copy table to clipboard');
button.setAttribute('title', 'Copy table with formatting for pasting into documents');
return button;
};
const createButtonContainer = () => {
const container = document.createElement('div');
container.className = 'flex justify-end mt-2 mb-2';
container.setAttribute('data-table-button-container', '1');
return container;
};
const processTableForCopy = (table) => {
const clone = table.cloneNode(true);
clone.querySelectorAll('[data-table-copy-button],[data-table-button-container]').forEach(el => el.remove());
clone.querySelectorAll('*').forEach(el => {
if (el.tagName.match(/^(TABLE|THEAD|TBODY|TFOOT|TR|TH|TD|CAPTION|COLGROUP|COL)$/)) {
el.removeAttribute('class');
el.removeAttribute('style');
el.removeAttribute('data-testid');
Array.from(el.attributes).forEach(attr => {
if (!['colspan', 'rowspan', 'scope', 'headers'].includes(attr.name.toLowerCase())) el.removeAttribute(attr.name);
});
} else {
const text = (el.textContent || '').trim();
if (text) {
el.replaceWith(document.createTextNode(text));
} else {
el.remove();
}
}
});
const cleanTable = document.createElement('table');
cleanTable.border = '1';
cleanTable.cellPadding = '4';
cleanTable.cellSpacing = '0';
Object.assign(cleanTable.style, { borderCollapse: 'collapse', width: '100%' });
let hasHeader = false;
clone.querySelectorAll('tr').forEach((row, rowIndex) => {
const newRow = document.createElement('tr');
row.querySelectorAll('th,td').forEach(cell => {
const isHeader = cell.tagName === 'TH' || (rowIndex === 0 && !hasHeader);
const newCell = document.createElement(isHeader ? 'th' : 'td');
if (isHeader) {
hasHeader = true;
Object.assign(newCell.style, { fontWeight: 'bold', backgroundColor: '#f0f0f0' });
}
Object.assign(newCell.style, {
border: '1px solid #ccc',
padding: '8px',
textAlign: 'left',
verticalAlign: 'top'
});
if (cell.hasAttribute('colspan')) newCell.setAttribute('colspan', cell.getAttribute('colspan'));
if (cell.hasAttribute('rowspan')) newCell.setAttribute('rowspan', cell.getAttribute('rowspan'));
newCell.textContent = (cell.textContent || '').trim();
newRow.appendChild(newCell);
});
cleanTable.appendChild(newRow);
});
return cleanTable;
};
const generateTSV = (table) =>
Array.from(table.querySelectorAll('tr'))
.map(row =>
Array.from(row.querySelectorAll('th,td'))
.map(cell => (cell.textContent || '').trim().replace(/[\t\n\r]+/g, ' ').replace(/\s+/g, ' '))
.join('\t')
)
.join('\n');
const copyTableToClipboard = async (table) => {
const processed = processTableForCopy(table);
const html = processed.outerHTML;
const tsv = generateTSV(processed);
try {
if (navigator.clipboard?.write) {
const fullHTML = `
<html>
<head>
<meta charset="utf-8">
<style>
table{border-collapse:collapse;width:100%}
th,td{border:1px solid #ccc;padding:8px;text-align:left;vertical-align:top}
th{font-weight:bold;background:#f0f0f0}
</style>
</head>
<body>${html}</body>
</html>
`.trim();
await navigator.clipboard.write([new ClipboardItem({
'text/html': new Blob([fullHTML], { type: 'text/html' }),
'text/plain': new Blob([tsv], { type: 'text/plain' })
})]);
} else if (navigator.clipboard?.writeText) {
await navigator.clipboard.writeText(tsv);
} else {
const textarea = document.createElement('textarea');
textarea.value = tsv;
document.body.appendChild(textarea);
textarea.select();
document.execCommand('copy');
document.body.removeChild(textarea);
}
showCopyFeedback(table);
} catch {
showCopyError(table);
}
};
const showCopyFeedback = (table) => {
const button = table.parentElement?.querySelector('[data-table-copy-button]');
if (!button) return;
const original = button.innerHTML;
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
Copied!
`;
button.style.color = 'rgb(34,197,94)';
setTimeout(() => {
button.innerHTML = original;
button.style.color = '';
}, 2000);
};
const showCopyError = (table) => {
const button = table.parentElement?.querySelector('[data-table-copy-button]');
if (!button) return;
const original = button.innerHTML;
button.innerHTML = `
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="10"></circle>
<line x1="15" y1="9" x2="9" y2="15"></line>
<line x1="9" y1="9" x2="15" y2="15"></line>
</svg>
Error
`;
button.style.color = 'rgb(239,68,68)';
setTimeout(() => {
button.innerHTML = original;
button.style.color = '';
}, 2000);
};
const addCopyButton = (table) => {
if (processedTables.has(table) || table.rows.length < 2) return;
const button = createCopyButton();
const container = createButtonContainer();
button.addEventListener('click', e => {
e.preventDefault();
e.stopPropagation();
copyTableToClipboard(table);
});
container.appendChild(button);
table.parentNode.insertBefore(container, table.nextSibling);
processedTables.add(table);
};
const scanTables = () => {
document.querySelectorAll(CONTENT_SELECTOR).forEach(area =>
area.querySelectorAll(TABLE_SELECTOR).forEach(addCopyButton)
);
};
const observer = new MutationObserver(mutations => {
const added = mutations.some(m =>
Array.from(m.addedNodes).some(node =>
node.nodeType === 1 && (node.tagName === 'TABLE' || node.querySelector?.(TABLE_SELECTOR))
)
);
if (added) setTimeout(scanTables, 100);
});
const init = () => {
scanTables();
observer.observe(document.documentElement, { childList: true, subtree: true });
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();