您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Add "Copy Table" buttons to tables in T3Chat to Office tools
- // ==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();
- }
- })();