Full document, spreadsheet, slideshow, and diagram tooling
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

fix: wire up dead import/export modules + fix paste crash

5 modules existed as dead code (never imported after TS migration merge):
- clipboard-paste/copy: rich paste from Excel/Google Sheets now works
- csv-export: CSV export now uses RFC 4180 module
- xlsx-export: XLSX export button now functional
- drop-overlay: drag-drop into editors now works

Also fixed #144: pasteAtSelection referenced undefined parsedRows variable.

Docs: added image paste handler, drop overlay for files+images.
Sheets: rich clipboard, export buttons, drop overlay.

+146 -30
+38 -11
src/docs/main.ts
··· 56 56 import { SLASH_COMMAND_ITEMS, SlashMenuState, filterCommands, PLACEHOLDER_EMPTY, PLACEHOLDER_BLOCK } from './slash-menu.js'; 57 57 import { createSlashCommands, getCommandExecutor } from './extensions/slash-commands.js'; 58 58 import { BlockHandleState, BLOCK_HANDLE_ACTIONS, TURN_INTO_ITEMS, filterTurnIntoItems, BLOCK_HANDLE_ICON, BLOCK_HANDLE_ADD_ICON } from './block-handle.js'; 59 + import { createDropOverlay } from '../drop-overlay.js'; 59 60 60 61 // --- Resolve document ID and encryption key --- 61 62 const pathParts = location.pathname.split('/').filter(Boolean); ··· 1018 1019 function handleImportedFile(file: File): void { 1019 1020 const ext = file.name.split('.').pop().toLowerCase(); 1020 1021 1022 + // Handle image files — read as data URL and insert 1023 + const imageExts = ['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg']; 1024 + if (imageExts.includes(ext)) { 1025 + const reader = new FileReader(); 1026 + reader.onload = (e) => { 1027 + const dataUrl = e.target.result as string; 1028 + editor.chain().focus().setImage({ src: dataUrl }).run(); 1029 + showToast(`Inserted image "${file.name}"`, 3000); 1030 + }; 1031 + reader.readAsDataURL(file); 1032 + return; 1033 + } 1034 + 1021 1035 // Handle .docx files via mammoth 1022 1036 if (ext === 'docx') { 1023 1037 importDocx(file, editor, showToast); ··· 1045 1059 function importFile() { 1046 1060 const input = document.createElement('input'); 1047 1061 input.type = 'file'; 1048 - input.accept = '.txt,.html,.htm,.md,.docx'; 1062 + input.accept = '.txt,.html,.htm,.md,.docx,.png,.jpg,.jpeg,.gif,.webp,.svg'; 1049 1063 input.addEventListener('change', () => { 1050 1064 if (input.files.length > 0) handleImportedFile(input.files[0]); 1051 1065 }); ··· 1053 1067 } 1054 1068 1055 1069 const editorContainer = document.querySelector('.editor-container'); 1056 - editorContainer.addEventListener('dragover', (e) => { 1057 - e.preventDefault(); 1058 - editorContainer.classList.add('drag-over'); 1059 - }); 1060 - editorContainer.addEventListener('dragleave', () => { 1061 - editorContainer.classList.remove('drag-over'); 1070 + createDropOverlay(editorContainer, { 1071 + acceptedExtensions: ['.docx', '.md', '.txt', '.html', '.htm', '.png', '.jpg', '.jpeg', '.gif', '.webp', '.svg'], 1072 + onDrop: handleImportedFile, 1073 + onReject: (msg) => showToast(msg, 4000), 1062 1074 }); 1063 - editorContainer.addEventListener('drop', (e) => { 1064 - e.preventDefault(); 1065 - editorContainer.classList.remove('drag-over'); 1066 - if (e.dataTransfer.files.length > 0) handleImportedFile(e.dataTransfer.files[0]); 1075 + 1076 + // Paste images from clipboard into the editor 1077 + editorContainer.addEventListener('paste', (e: ClipboardEvent) => { 1078 + const items = e.clipboardData?.items; 1079 + if (!items) return; 1080 + for (const item of items) { 1081 + if (item.type.startsWith('image/')) { 1082 + e.preventDefault(); 1083 + const file = item.getAsFile(); 1084 + if (!file) return; 1085 + const reader = new FileReader(); 1086 + reader.onload = (ev) => { 1087 + const dataUrl = ev.target.result as string; 1088 + editor.chain().focus().setImage({ src: dataUrl }).run(); 1089 + }; 1090 + reader.readAsDataURL(file); 1091 + return; 1092 + } 1093 + } 1067 1094 }); 1068 1095 1069 1096 function printDocument() { window.print(); }
+108 -19
src/sheets/main.ts
··· 25 25 import { extractFormulaRanges, assignRangeColors, renderGridHighlights, clearGridHighlights } from './range-highlight.js'; 26 26 import { detectCurrentFunction, renderTooltip, hideTooltip } from './formula-tooltip.js'; 27 27 import { extractFormat, applyFormat } from './format-painter.js'; 28 + import { parseClipboardHtml, parseClipboardTsv } from './clipboard-paste.js'; 29 + import { buildCopyHtml, buildCopyTsv } from './clipboard-copy.js'; 30 + import { exportToCsv, downloadCsv } from './csv-export.js'; 31 + import { exportToXlsx, downloadXlsx } from './xlsx-export.js'; 32 + import { createDropOverlay } from '../drop-overlay.js'; 28 33 29 34 // --- Constants --- 30 35 const DEFAULT_ROWS = 100; ··· 846 851 document.addEventListener('paste', (e) => { 847 852 if (editingCell || document.activeElement === formulaInput) return; 848 853 e.preventDefault(); 849 - pasteAtSelection(e.clipboardData.getData('text/plain')); 854 + 855 + // Try rich HTML paste first (from Excel / Google Sheets) 856 + const html = e.clipboardData.getData('text/html'); 857 + if (html) { 858 + const parsed = parseClipboardHtml(html); 859 + if (parsed && parsed.rows.length > 0) { 860 + pasteRichRows(parsed.rows); 861 + return; 862 + } 863 + } 864 + 865 + // Fallback: try TSV (tab-separated) from plain text 866 + const plain = e.clipboardData.getData('text/plain'); 867 + if (plain) { 868 + const parsed = parseClipboardTsv(plain); 869 + if (parsed && parsed.rows.length > 0) { 870 + pasteRichRows(parsed.rows); 871 + return; 872 + } 873 + // Final fallback: original plain-text paste 874 + pasteAtSelection(plain); 875 + } 850 876 }); 851 877 852 878 function moveSelection(dCol, dRow) { ··· 892 918 893 919 function copySelection() { 894 920 if (!selectionRange) return; 895 - const { startCol, startRow, endCol, endRow } = normalizeRange(selectionRange); 896 - const rows = []; 897 - for (let r = startRow; r <= endRow; r++) { 898 - const cols = []; 899 - for (let c = startCol; c <= endCol; c++) { 900 - const data = getCellData(cellId(c, r)); 901 - cols.push(data?.f ? '=' + data.f : (data?.v ?? '')); 902 - } 903 - rows.push(cols.join('\t')); 921 + const sel = normalizeRange(selectionRange); 922 + const getCellDataById = (id) => getCellData(id); 923 + 924 + const html = buildCopyHtml(getCellDataById, sel, cellId); 925 + const tsv = buildCopyTsv(getCellDataById, sel, cellId); 926 + 927 + // Write both HTML and plain-text representations to the clipboard 928 + try { 929 + const htmlBlob = new Blob([html], { type: 'text/html' }); 930 + const textBlob = new Blob([tsv], { type: 'text/plain' }); 931 + navigator.clipboard.write([ 932 + new ClipboardItem({ 933 + 'text/html': htmlBlob, 934 + 'text/plain': textBlob, 935 + }), 936 + ]); 937 + } catch { 938 + // Fallback: plain text only (e.g. older browsers / insecure context) 939 + navigator.clipboard.writeText(tsv); 904 940 } 905 - navigator.clipboard.writeText(rows.join('\n')); 906 941 } 907 942 908 943 function pasteAtSelection(text) { ··· 910 945 const sc = selectedCell.col; 911 946 const sr = selectedCell.row; 912 947 ydoc.transact(() => { 913 - for (let r = 0; r < parsedRows.length; r++) { 948 + for (let r = 0; r < lines.length; r++) { 914 949 const cols = lines[r].split('\t'); 915 950 for (let c = 0; c < cols.length; c++) { 916 951 const val = cols[c].trim(); ··· 924 959 refreshVisibleCells(); 925 960 } 926 961 962 + /** 963 + * Paste rich parsed rows (from parseClipboardHtml or parseClipboardTsv) 964 + * preserving values, formulas, and styles. 965 + */ 966 + function pasteRichRows(rows) { 967 + const sc = selectedCell.col; 968 + const sr = selectedCell.row; 969 + ydoc.transact(() => { 970 + for (let r = 0; r < rows.length; r++) { 971 + for (let c = 0; c < rows[r].length; c++) { 972 + const cell = rows[r][c]; 973 + const id = cellId(sc + c, sr + r); 974 + const data: any = { v: cell.value, f: cell.formula || '' }; 975 + if (cell.style && Object.keys(cell.style).length > 0) { 976 + data.s = cell.style; 977 + } 978 + setCellData(id, data); 979 + } 980 + } 981 + }); 982 + evalCache.clear(); invalidateRecalcEngine(); 983 + refreshVisibleCells(); 984 + } 985 + 927 986 // --- Visual updates (#18: improved range selection) --- 928 987 function updateSelectionVisuals() { 929 988 grid.querySelectorAll('.selected').forEach(el => el.classList.remove('selected')); ··· 1550 1609 return lines.join('\n'); 1551 1610 } 1552 1611 1553 - function exportCSV() { const name = getActiveSheet().get('name') || 'sheet'; downloadFile(sheetToDelimited(','), name + '.csv', 'text/csv;charset=utf-8'); } 1612 + function exportCSV() { 1613 + const sheet = getActiveSheet(); 1614 + const rowCount = sheet.get('rowCount') || DEFAULT_ROWS; 1615 + const colCount = sheet.get('colCount') || DEFAULT_COLS; 1616 + const name = sheet.get('name') || 'sheet'; 1617 + const content = exportToCsv((r, c) => { 1618 + const data = getCellData(cellId(c, r)); 1619 + if (!data) return ''; 1620 + return data.f ? '=' + data.f : String(data.v ?? ''); 1621 + }, rowCount, colCount); 1622 + downloadCsv(content, name, ','); 1623 + } 1554 1624 function exportTSV() { const name = getActiveSheet().get('name') || 'sheet'; downloadFile(sheetToDelimited('\t'), name + '.tsv', 'text/tab-separated-values;charset=utf-8'); } 1625 + async function exportXLSX() { 1626 + const sheet = getActiveSheet(); 1627 + const rowCount = sheet.get('rowCount') || DEFAULT_ROWS; 1628 + const colCount = sheet.get('colCount') || DEFAULT_COLS; 1629 + const name = sheet.get('name') || 'sheet'; 1630 + const XLSX = await import('xlsx'); 1631 + const buffer = exportToXlsx( 1632 + (r, c) => { 1633 + const data = getCellData(cellId(c, r)); 1634 + if (!data) return null; 1635 + return { v: data.v, f: data.f || '', s: data.s || {} }; 1636 + }, 1637 + rowCount, 1638 + colCount, 1639 + (c) => getColWidth(c), 1640 + name, 1641 + XLSX, 1642 + ); 1643 + downloadXlsx(buffer, name + '.xlsx'); 1644 + } 1555 1645 1556 1646 function parseCSVLine(line) { 1557 1647 const fields = []; let field = ''; let inQuotes = false; ··· 1671 1761 1672 1762 // Toolbar button bindings for export/import/print 1673 1763 document.getElementById('tb-export-csv').addEventListener('click', () => { exportCSV(); closeAllDropdowns(); }); 1764 + document.getElementById('tb-export-xlsx').addEventListener('click', () => { exportXLSX(); closeAllDropdowns(); }); 1674 1765 document.getElementById('tb-import').addEventListener('click', () => { importCSV(); closeAllDropdowns(); }); 1675 1766 document.getElementById('tb-print').addEventListener('click', () => { printSheet(); closeAllDropdowns(); }); 1676 1767 1677 1768 // --- Drag-and-drop import --- 1678 - sheetContainer.addEventListener('dragover', (e) => { e.preventDefault(); e.dataTransfer.dropEffect = 'copy'; sheetContainer.classList.add('drag-over'); }); 1679 - sheetContainer.addEventListener('dragleave', () => { sheetContainer.classList.remove('drag-over'); }); 1680 - sheetContainer.addEventListener('drop', (e) => { 1681 - e.preventDefault(); sheetContainer.classList.remove('drag-over'); 1682 - const file = e.dataTransfer.files[0]; if (!file) return; 1683 - handleImportFile(file); 1769 + createDropOverlay(sheetContainer, { 1770 + acceptedExtensions: ['.xlsx', '.xls', '.csv', '.tsv', '.txt'], 1771 + onDrop: handleImportFile, 1772 + onReject: (msg) => showToast(msg, 4000), 1684 1773 }); 1685 1774 1686 1775 // --- React to Yjs changes ---