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

Configure Feed

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

Merge pull request 'fix: selection perf + surface silent errors' (#73) from fix/selection-perf into main

scott 37dc6bc6 b7327cdc

+33 -17
+3 -1
src/docs/main.ts
··· 918 918 method: 'PUT', 919 919 headers: { 'Content-Type': 'application/json' }, 920 920 body: JSON.stringify({ name_encrypted: b64 }), 921 - }); 921 + }) 922 + .then(r => { if (!r.ok) throw new Error(); }) 923 + .catch(() => showToast('Failed to save title')); 922 924 }, 500); 923 925 }); 924 926
+2 -2
src/landing.ts
··· 278 278 if (expired.length > 0) { 279 279 // Permanently delete expired docs from server 280 280 for (const id of expired) { 281 - fetch(`/api/documents/${id}`, { method: 'DELETE' }).catch(() => {}); 281 + fetch(`/api/documents/${id}`, { method: 'DELETE' }).catch(() => showToast('Failed to delete document from server', 4000, true)); 282 282 // Also clean up keys 283 283 const keys = JSON.parse(localStorage.getItem('tools-keys') || '{}'); 284 284 delete keys[id]; ··· 585 585 if (!confirm(`Permanently delete all ${trashedDocs.length} trashed documents? This cannot be undone.`)) return; 586 586 const k = JSON.parse(localStorage.getItem('tools-keys') || '{}'); 587 587 for (const doc of trashedDocs) { 588 - await fetch(`/api/documents/${doc.id}`, { method: 'DELETE' }).catch(() => {}); 588 + await fetch(`/api/documents/${doc.id}`, { method: 'DELETE' }).catch(() => showToast('Failed to delete document from server', 4000, true)); 589 589 delete k[doc.id]; 590 590 } 591 591 localStorage.setItem('tools-keys', JSON.stringify(k));
+28 -14
src/sheets/main.ts
··· 1633 1633 } 1634 1634 1635 1635 // --- Visual updates (#18: improved range selection) --- 1636 + // Track previously styled elements to avoid full-grid querySelectorAll on every selection change 1637 + const selectionClasses = ['selected', 'in-range', 'range-top', 'range-bottom', 'range-left', 'range-right', 'col-selected', 'row-selected'] as const; 1638 + let prevSelectionEls: Element[] = []; 1639 + 1640 + function clearPrevSelection() { 1641 + for (const el of prevSelectionEls) { 1642 + el.classList.remove(...selectionClasses); 1643 + } 1644 + prevSelectionEls = []; 1645 + } 1646 + 1647 + function getCellEl(col: number, row: number): Element | null { 1648 + return grid.querySelector(`td[data-col="${col}"][data-row="${row}"]`); 1649 + } 1650 + 1636 1651 function updateSelectionVisuals() { 1637 - grid.querySelectorAll('.selected').forEach(el => el.classList.remove('selected')); 1638 - grid.querySelectorAll('.in-range').forEach(el => el.classList.remove('in-range')); 1639 - grid.querySelectorAll('.range-top').forEach(el => el.classList.remove('range-top')); 1640 - grid.querySelectorAll('.range-bottom').forEach(el => el.classList.remove('range-bottom')); 1641 - grid.querySelectorAll('.range-left').forEach(el => el.classList.remove('range-left')); 1642 - grid.querySelectorAll('.range-right').forEach(el => el.classList.remove('range-right')); 1643 - grid.querySelectorAll('.col-selected').forEach(el => el.classList.remove('col-selected')); 1644 - grid.querySelectorAll('.row-selected').forEach(el => el.classList.remove('row-selected')); 1652 + clearPrevSelection(); 1645 1653 1646 - const currentTd = grid.querySelector('td[data-col="' + selectedCell.col + '"][data-row="' + selectedCell.row + '"]'); 1647 - if (currentTd) currentTd.classList.add('selected'); 1654 + const currentTd = getCellEl(selectedCell.col, selectedCell.row); 1655 + if (currentTd) { 1656 + currentTd.classList.add('selected'); 1657 + prevSelectionEls.push(currentTd); 1658 + } 1648 1659 1649 1660 if (selectionRange) { 1650 1661 const { startCol, startRow, endCol, endRow } = normalizeRange(selectionRange); 1651 1662 const isMultiCell = startCol !== endCol || startRow !== endRow; 1652 1663 for (let r = startRow; r <= endRow; r++) { 1653 1664 for (let c = startCol; c <= endCol; c++) { 1654 - const td = grid.querySelector('td[data-col="' + c + '"][data-row="' + r + '"]'); 1665 + const td = getCellEl(c, r); 1655 1666 if (!td) continue; 1656 1667 if (!(c === selectedCell.col && r === selectedCell.row)) td.classList.add('in-range'); 1657 1668 if (isMultiCell) { ··· 1660 1671 if (c === startCol) td.classList.add('range-left'); 1661 1672 if (c === endCol) td.classList.add('range-right'); 1662 1673 } 1674 + prevSelectionEls.push(td); 1663 1675 } 1664 1676 } 1665 1677 for (let c = startCol; c <= endCol; c++) { 1666 1678 const th = grid.querySelector('thead th[data-col="' + c + '"]'); 1667 - if (th) th.classList.add('col-selected'); 1679 + if (th) { th.classList.add('col-selected'); prevSelectionEls.push(th); } 1668 1680 } 1669 1681 for (let r = startRow; r <= endRow; r++) { 1670 1682 const th = grid.querySelector('th.row-header[data-row="' + r + '"]'); 1671 - if (th) th.classList.add('row-selected'); 1683 + if (th) { th.classList.add('row-selected'); prevSelectionEls.push(th); } 1672 1684 } 1673 1685 if (isMultiCell) { 1674 1686 cellAddressInput.value = cellId(startCol, startRow) + ':' + cellId(endCol, endRow); ··· 2619 2631 titleSaveTimeout = setTimeout(async () => { 2620 2632 const encrypted = await encryptString(titleInput.value, cryptoKey); 2621 2633 const b64 = btoa(String.fromCharCode(...encrypted)); 2622 - fetch('/api/documents/' + docId + '/name', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name_encrypted: b64 }) }); 2634 + fetch('/api/documents/' + docId + '/name', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name_encrypted: b64 }) }) 2635 + .then(r => { if (!r.ok) throw new Error(); }) 2636 + .catch(() => showToast('Failed to save title')); 2623 2637 }, 500); 2624 2638 }); 2625 2639