codebook / potato /static /js /format-displays.js
davidjurgens's picture
Deploy: Potato — Codebook Annotation
aceb1b2 verified
Raw
History Blame Contribute Delete
11.3 kB
/**
* 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 = `
<svg viewBox="0 0 24 24" width="16" height="16">
<path fill="currentColor" d="M16 1H4c-1.1 0-2 .9-2 2v14h2V3h12V1zm3 4H8c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h11c1.1 0 2-.9 2-2V7c0-1.1-.9-2-2-2zm0 16H8V7h11v14z"/>
</svg>
`;
}, 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;