/** * Format Display Components * * JavaScript for spreadsheet and code display interactivity. */ // ============================================ // Spreadsheet Display // ============================================ /** * Initialize all spreadsheet displays on the page. */ function initSpreadsheetDisplays() { const containers = document.querySelectorAll('.spreadsheet-display'); containers.forEach(container => { if (!container.dataset.initialized) { new SpreadsheetController(container); container.dataset.initialized = 'true'; } }); } /** * Spreadsheet controller class. */ class SpreadsheetController { constructor(container) { this.container = container; this.table = container.querySelector('.spreadsheet-table'); this.mode = container.dataset.annotationMode || 'row'; this.selectable = container.dataset.selectable !== 'false'; this.selectedRows = new Set(); this.selectedCells = new Set(); this.init(); } init() { if (!this.table || !this.selectable) return; if (this.mode === 'row') { this.initRowSelection(); } else if (this.mode === 'cell') { this.initCellSelection(); } this.initSorting(); } initRowSelection() { const rows = this.table.querySelectorAll('tbody tr.selectable-row'); rows.forEach(row => { row.addEventListener('click', (e) => { // Don't toggle if clicking checkbox directly if (e.target.type === 'checkbox') return; const rowIdx = parseInt(row.dataset.row); this.toggleRowSelection(rowIdx, row); }); }); // Handle checkboxes const checkboxes = this.table.querySelectorAll('.row-select'); checkboxes.forEach(checkbox => { checkbox.addEventListener('change', (e) => { const rowIdx = parseInt(checkbox.dataset.row); const row = this.table.querySelector(`tr[data-row="${rowIdx}"]`); if (e.target.checked) { this.selectRow(rowIdx, row); } else { this.deselectRow(rowIdx, row); } }); }); } toggleRowSelection(rowIdx, row) { if (this.selectedRows.has(rowIdx)) { this.deselectRow(rowIdx, row); } else { this.selectRow(rowIdx, row); } } selectRow(rowIdx, row) { this.selectedRows.add(rowIdx); row.classList.add('selected'); const checkbox = row.querySelector('.row-select'); if (checkbox) checkbox.checked = true; this.updateSelectionCount(); this.emitSelectionChange(); } deselectRow(rowIdx, row) { this.selectedRows.delete(rowIdx); row.classList.remove('selected'); const checkbox = row.querySelector('.row-select'); if (checkbox) checkbox.checked = false; this.updateSelectionCount(); this.emitSelectionChange(); } initCellSelection() { const cells = this.table.querySelectorAll('td.selectable-cell'); cells.forEach(cell => { cell.addEventListener('click', () => { const cellRef = cell.dataset.cellRef; this.toggleCellSelection(cellRef, cell); }); }); } toggleCellSelection(cellRef, cell) { if (this.selectedCells.has(cellRef)) { this.selectedCells.delete(cellRef); cell.classList.remove('selected'); } else { this.selectedCells.add(cellRef); cell.classList.add('selected'); } this.emitSelectionChange(); } updateSelectionCount() { const summary = this.container.querySelector('.selected-count'); if (summary) { summary.textContent = this.selectedRows.size; } } emitSelectionChange() { const event = new CustomEvent('spreadsheet:selection-change', { detail: { mode: this.mode, selectedRows: Array.from(this.selectedRows), selectedCells: Array.from(this.selectedCells), }, bubbles: true, }); this.container.dispatchEvent(event); } initSorting() { const sortableHeaders = this.table.querySelectorAll('th[data-sortable="true"]'); sortableHeaders.forEach(th => { th.addEventListener('click', () => { const colIdx = parseInt(th.dataset.col); this.sortByColumn(colIdx, th); }); }); } sortByColumn(colIdx, header) { const tbody = this.table.querySelector('tbody'); const rows = Array.from(tbody.querySelectorAll('tr')); // Determine sort direction const currentDir = header.dataset.sortDir || 'none'; const newDir = currentDir === 'asc' ? 'desc' : 'asc'; // Reset other headers this.table.querySelectorAll('th').forEach(th => { th.dataset.sortDir = 'none'; }); header.dataset.sortDir = newDir; // Sort rows rows.sort((a, b) => { const aCell = a.querySelector(`td[data-col="${colIdx}"]`); const bCell = b.querySelector(`td[data-col="${colIdx}"]`); const aVal = aCell ? aCell.textContent.trim() : ''; const bVal = bCell ? bCell.textContent.trim() : ''; // Try numeric sort first const aNum = parseFloat(aVal); const bNum = parseFloat(bVal); if (!isNaN(aNum) && !isNaN(bNum)) { return newDir === 'asc' ? aNum - bNum : bNum - aNum; } // Fall back to string sort return newDir === 'asc' ? aVal.localeCompare(bVal) : bVal.localeCompare(aVal); }); // Re-append rows in sorted order rows.forEach(row => tbody.appendChild(row)); } getSelection() { return { mode: this.mode, selectedRows: Array.from(this.selectedRows), selectedCells: Array.from(this.selectedCells), }; } clearSelection() { this.selectedRows.clear(); this.selectedCells.clear(); this.table.querySelectorAll('.selected').forEach(el => { el.classList.remove('selected'); }); this.table.querySelectorAll('.row-select').forEach(cb => { cb.checked = false; }); this.updateSelectionCount(); this.emitSelectionChange(); } } // ============================================ // Code Display // ============================================ /** * Initialize all code displays on the page. */ function initCodeDisplays() { const containers = document.querySelectorAll('.code-display'); containers.forEach(container => { if (!container.dataset.initialized) { new CodeController(container); container.dataset.initialized = 'true'; } }); } /** * Code display controller class. */ class CodeController { constructor(container) { this.container = container; this.copyBtn = container.querySelector('.code-copy-btn'); this.codeContent = container.querySelector('.code-content'); this.init(); } init() { if (this.copyBtn) { this.copyBtn.addEventListener('click', () => this.copyToClipboard()); } // Line selection for span annotation if (this.container.classList.contains('span-target-code')) { this.initLineSelection(); } } copyToClipboard() { // Get plain text content let text = ''; const lines = this.container.querySelectorAll('.line-content code'); lines.forEach((line, idx) => { if (idx > 0) text += '\n'; text += line.textContent; }); navigator.clipboard.writeText(text).then(() => { this.copyBtn.classList.add('copied'); this.copyBtn.innerHTML = '✓'; setTimeout(() => { this.copyBtn.classList.remove('copied'); this.copyBtn.innerHTML = ` `; }, 2000); }).catch(err => { console.error('Copy failed:', err); }); } initLineSelection() { const lines = this.container.querySelectorAll('.code-line'); lines.forEach(line => { line.addEventListener('click', () => { line.classList.toggle('span-selected'); this.emitLineSelection(); }); }); } emitLineSelection() { const selectedLines = []; this.container.querySelectorAll('.code-line.span-selected').forEach(line => { const lineNum = line.querySelector('.line-number'); if (lineNum) { selectedLines.push(parseInt(lineNum.dataset.line)); } }); const event = new CustomEvent('code:line-selection', { detail: { selectedLines }, bubbles: true, }); this.container.dispatchEvent(event); } getCode() { let text = ''; const lines = this.container.querySelectorAll('.line-content code'); lines.forEach((line, idx) => { if (idx > 0) text += '\n'; text += line.textContent; }); return text; } } // ============================================ // Document Display // ============================================ /** * Initialize document display features. */ function initDocumentDisplays() { // Outline navigation document.querySelectorAll('.outline-link').forEach(link => { link.addEventListener('click', (e) => { e.preventDefault(); const offset = parseInt(link.dataset.offset); // Find element at offset and scroll to it // This would require coordination with the annotation system console.log('Navigate to offset:', offset); }); }); } // ============================================ // Auto-initialization // ============================================ function initAllFormatDisplays() { initSpreadsheetDisplays(); initCodeDisplays(); initDocumentDisplays(); } // Auto-initialize on DOM ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initAllFormatDisplays); } else { initAllFormatDisplays(); } // Re-initialize when new content is loaded document.addEventListener('potato:content-loaded', initAllFormatDisplays); // Export for external use window.SpreadsheetController = SpreadsheetController; window.CodeController = CodeController; window.initSpreadsheetDisplays = initSpreadsheetDisplays; window.initCodeDisplays = initCodeDisplays; window.initDocumentDisplays = initDocumentDisplays;