/* ============================================================================= GB SHEET - Excel-like Spreadsheet JavaScript General Bots Suite Component ============================================================================= */ (function () { "use strict"; // ============================================================================= // CONFIGURATION // ============================================================================= const CONFIG = { COLS: 26, ROWS: 100, COL_WIDTH: 100, ROW_HEIGHT: 24, MAX_HISTORY: 50, AUTOSAVE_DELAY: 3000, WS_RECONNECT_DELAY: 5000, }; // ============================================================================= // STATE // ============================================================================= const state = { sheetId: null, sheetName: "Untitled Spreadsheet", worksheets: [{ name: "Sheet1", data: {} }], activeWorksheet: 0, selection: { start: { row: 0, col: 0 }, end: { row: 0, col: 0 }, }, activeCell: { row: 0, col: 0 }, clipboard: null, clipboardMode: null, history: [], historyIndex: -1, zoom: 1, collaborators: [], ws: null, isEditing: false, isSelecting: false, selectionStart: null, isDirty: false, autoSaveTimer: null, }; // ============================================================================= // DOM ELEMENTS // ============================================================================= const elements = { container: null, sidebar: null, sheetList: null, sheetName: null, columnHeaders: null, rowHeaders: null, cells: null, cellsContainer: null, formulaInput: null, cellAddress: null, formulaPreview: null, worksheetTabs: null, collaborators: null, contextMenu: null, tabContextMenu: null, shareModal: null, functionModal: null, chartModal: null, cursorIndicators: null, }; // ============================================================================= // INITIALIZATION // ============================================================================= function init() { cacheElements(); renderGrid(); bindEvents(); loadFromUrlParams(); connectWebSocket(); updateCellAddress(); renderWorksheetTabs(); } function cacheElements() { elements.container = document.querySelector(".sheet-container"); elements.sidebar = document.getElementById("sheet-sidebar"); elements.sheetList = document.getElementById("sheet-list"); elements.sheetName = document.getElementById("sheet-name"); elements.columnHeaders = document.getElementById("column-headers"); elements.rowHeaders = document.getElementById("row-headers"); elements.cells = document.getElementById("cells"); elements.cellsContainer = document.getElementById("cells-container"); elements.formulaInput = document.getElementById("formula-input"); elements.cellAddress = document.getElementById("cell-address"); elements.formulaPreview = document.getElementById("formula-preview"); elements.worksheetTabs = document.getElementById("worksheet-tabs"); elements.collaborators = document.getElementById("collaborators"); elements.contextMenu = document.getElementById("context-menu"); elements.tabContextMenu = document.getElementById("tab-context-menu"); elements.shareModal = document.getElementById("share-modal"); elements.functionModal = document.getElementById("function-modal"); elements.chartModal = document.getElementById("chart-modal"); elements.cursorIndicators = document.getElementById("cursor-indicators"); } // ============================================================================= // GRID RENDERING // ============================================================================= function renderGrid() { if (!elements.columnHeaders || !elements.rowHeaders || !elements.cells) return; elements.columnHeaders.innerHTML = ""; for (let c = 0; c < CONFIG.COLS; c++) { const header = document.createElement("div"); header.className = "column-header"; header.textContent = getColName(c); header.dataset.col = c; header.innerHTML += '
'; elements.columnHeaders.appendChild(header); } elements.rowHeaders.innerHTML = ""; for (let r = 0; r < CONFIG.ROWS; r++) { const header = document.createElement("div"); header.className = "row-header"; header.textContent = r + 1; header.dataset.row = r; elements.rowHeaders.appendChild(header); } elements.cells.innerHTML = ""; elements.cells.style.gridTemplateColumns = `repeat(${CONFIG.COLS}, ${CONFIG.COL_WIDTH}px)`; for (let r = 0; r < CONFIG.ROWS; r++) { for (let c = 0; c < CONFIG.COLS; c++) { const cell = document.createElement("div"); cell.className = "cell"; cell.dataset.row = r; cell.dataset.col = c; cell.id = `cell-${r}-${c}`; elements.cells.appendChild(cell); } } selectCell(0, 0); } function renderAllCells() { const ws = state.worksheets[state.activeWorksheet]; if (!ws) return; for (let r = 0; r < CONFIG.ROWS; r++) { for (let c = 0; c < CONFIG.COLS; c++) { renderCell(r, c); } } } function renderCell(row, col) { const cell = document.getElementById(`cell-${row}-${col}`); if (!cell) return; const data = getCellData(row, col); if (data) { let displayValue = data.value || ""; if (displayValue.startsWith("=")) { displayValue = evaluateFormula(displayValue, row, col); } cell.textContent = displayValue; if (data.format) { applyFormatToCell(cell, data.format); } } else { cell.textContent = ""; cell.style.cssText = ""; } } function applyFormatToCell(cell, format) { if (format.bold) cell.style.fontWeight = "bold"; if (format.italic) cell.style.fontStyle = "italic"; if (format.underline) cell.style.textDecoration = "underline"; if (format.strikethrough) { cell.style.textDecoration = cell.style.textDecoration ? cell.style.textDecoration + " line-through" : "line-through"; } if (format.fontFamily) cell.style.fontFamily = format.fontFamily; if (format.fontSize) cell.style.fontSize = format.fontSize + "px"; if (format.color) cell.style.color = format.color; if (format.backgroundColor) cell.style.backgroundColor = format.backgroundColor; if (format.textAlign) cell.style.textAlign = format.textAlign; } // ============================================================================= // COLUMN/ROW UTILITIES // ============================================================================= function getColName(col) { let name = ""; col++; while (col > 0) { col--; name = String.fromCharCode(65 + (col % 26)) + name; col = Math.floor(col / 26); } return name; } function parseColName(name) { let col = 0; for (let i = 0; i < name.length; i++) { col = col * 26 + (name.charCodeAt(i) - 64); } return col - 1; } function getCellRef(row, col) { return getColName(col) + (row + 1); } function parseCellRef(ref) { const match = ref.match(/^([A-Z]+)(\d+)$/i); if (!match) return null; return { row: parseInt(match[2]) - 1, col: parseColName(match[1].toUpperCase()), }; } // ============================================================================= // EVENT BINDING // ============================================================================= function bindEvents() { if (elements.cells) { elements.cells.addEventListener("mousedown", handleCellMouseDown); elements.cells.addEventListener("dblclick", handleCellDoubleClick); elements.cells.addEventListener("contextmenu", handleContextMenu); } document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); document.addEventListener("keydown", handleKeyDown); document.addEventListener("click", hideContextMenus); if (elements.formulaInput) { elements.formulaInput.addEventListener("keydown", handleFormulaKey); elements.formulaInput.addEventListener("input", updateFormulaPreview); } if (elements.columnHeaders) { elements.columnHeaders.addEventListener("click", handleColumnHeaderClick); } if (elements.rowHeaders) { elements.rowHeaders.addEventListener("click", handleRowHeaderClick); } window.addEventListener("beforeunload", handleBeforeUnload); } // ============================================================================= // CELL SELECTION // ============================================================================= function handleCellMouseDown(e) { const cell = e.target.closest(".cell"); if (!cell) return; const row = parseInt(cell.dataset.row); const col = parseInt(cell.dataset.col); if (e.shiftKey) { extendSelection(row, col); } else { state.isSelecting = true; state.selectionStart = { row, col }; selectCell(row, col); } } function handleMouseMove(e) { if (!state.isSelecting || !state.selectionStart) return; const cell = document.elementFromPoint(e.clientX, e.clientY); if (!cell || !cell.classList.contains("cell")) return; const row = parseInt(cell.dataset.row); const col = parseInt(cell.dataset.col); extendSelection(row, col); } function handleMouseUp() { state.isSelecting = false; state.selectionStart = null; } function handleCellDoubleClick(e) { const cell = e.target.closest(".cell"); if (!cell) return; const row = parseInt(cell.dataset.row); const col = parseInt(cell.dataset.col); startEditing(row, col); } function selectCell(row, col) { clearSelection(); state.activeCell = { row, col }; state.selection = { start: { row, col }, end: { row, col }, }; const cell = document.getElementById(`cell-${row}-${col}`); if (cell) { cell.classList.add("selected"); } updateCellAddress(); updateFormulaBar(); updateSelectionInfo(); } function extendSelection(row, col) { if (!state.selectionStart) { state.selectionStart = { ...state.activeCell }; } const start = state.selectionStart; state.selection = { start: { row: Math.min(start.row, row), col: Math.min(start.col, col), }, end: { row: Math.max(start.row, row), col: Math.max(start.col, col), }, }; clearSelection(); for (let r = state.selection.start.row; r <= state.selection.end.row; r++) { for ( let c = state.selection.start.col; c <= state.selection.end.col; c++ ) { const cell = document.getElementById(`cell-${r}-${c}`); if (cell) { if (r === state.activeCell.row && c === state.activeCell.col) { cell.classList.add("selected"); } else { cell.classList.add("in-range"); } } } } updateSelectionInfo(); updateCalculationResult(); } function clearSelection() { document .querySelectorAll(".cell.selected, .cell.in-range") .forEach((cell) => { cell.classList.remove("selected", "in-range"); }); } function handleColumnHeaderClick(e) { const header = e.target.closest(".column-header"); if (!header) return; const col = parseInt(header.dataset.col); state.selection = { start: { row: 0, col }, end: { row: CONFIG.ROWS - 1, col }, }; state.activeCell = { row: 0, col }; clearSelection(); for (let r = 0; r < CONFIG.ROWS; r++) { const cell = document.getElementById(`cell-${r}-${col}`); if (cell) { cell.classList.add(r === 0 ? "selected" : "in-range"); } } header.classList.add("selected"); updateCellAddress(); updateCalculationResult(); } function handleRowHeaderClick(e) { const header = e.target.closest(".row-header"); if (!header) return; const row = parseInt(header.dataset.row); state.selection = { start: { row, col: 0 }, end: { row, col: CONFIG.COLS - 1 }, }; state.activeCell = { row, col: 0 }; clearSelection(); for (let c = 0; c < CONFIG.COLS; c++) { const cell = document.getElementById(`cell-${row}-${c}`); if (cell) { cell.classList.add(c === 0 ? "selected" : "in-range"); } } header.classList.add("selected"); updateCellAddress(); updateCalculationResult(); } // ============================================================================= // CELL EDITING // ============================================================================= function startEditing(row, col) { const cell = document.getElementById(`cell-${row}-${col}`); if (!cell) return; state.isEditing = true; const data = getCellData(row, col); cell.classList.add("editing"); const input = document.createElement("input"); input.type = "text"; input.className = "cell-input"; input.value = data ? data.value || "" : ""; input.addEventListener("blur", () => finishEditing(row, col)); input.addEventListener("keydown", (e) => { if (e.key === "Enter") { finishEditing(row, col); navigateCell(1, 0); } else if (e.key === "Tab") { e.preventDefault(); finishEditing(row, col); navigateCell(0, e.shiftKey ? -1 : 1); } else if (e.key === "Escape") { cancelEditing(row, col); } }); cell.innerHTML = ""; cell.appendChild(input); input.focus(); input.select(); } function finishEditing(row, col) { const cell = document.getElementById(`cell-${row}-${col}`); if (!cell) return; const input = cell.querySelector(".cell-input"); if (input) { const value = input.value; setCellValue(row, col, value); broadcastChange(row, col, value); } cell.classList.remove("editing"); state.isEditing = false; renderCell(row, col); } function cancelEditing(row, col) { const cell = document.getElementById(`cell-${row}-${col}`); if (!cell) return; cell.classList.remove("editing"); state.isEditing = false; renderCell(row, col); } // ============================================================================= // CELL DATA // ============================================================================= function setCellValue(row, col, value) { const ws = state.worksheets[state.activeWorksheet]; const key = `${row},${col}`; if (!value && ws.data[key]) { delete ws.data[key]; } else if (value) { if (!ws.data[key]) { ws.data[key] = {}; } ws.data[key].value = value; } state.isDirty = true; scheduleAutoSave(); saveToHistory(); } function getCellData(row, col) { const ws = state.worksheets[state.activeWorksheet]; return ws ? ws.data[`${row},${col}`] : null; } function getCellValue(row, col) { const data = getCellData(row, col); return data ? data.value || "" : ""; } // ============================================================================= // FORMULA EVALUATION // ============================================================================= function evaluateFormula(formula, sourceRow, sourceCol) { if (!formula.startsWith("=")) return formula; try { let expr = formula.substring(1); expr = expr.replace(/([A-Z]+\d+):([A-Z]+\d+)/gi, (match, start, end) => { return JSON.stringify(parseRange(start, end)); }); expr = expr.replace(/([A-Z]+)(\d+)/gi, (match, col, row) => { const r = parseInt(row) - 1; const c = parseColName(col.toUpperCase()); const val = getCellValue(r, c); const num = parseFloat(val); return isNaN(num) ? `"${val}"` : num; }); expr = expr.replace(/SUM\s*\(\s*(\[.*?\])\s*\)/gi, (match, arr) => { return `sumRange(${arr})`; }); expr = expr.replace(/AVERAGE\s*\(\s*(\[.*?\])\s*\)/gi, (match, arr) => { return `averageRange(${arr})`; }); expr = expr.replace(/COUNT\s*\(\s*(\[.*?\])\s*\)/gi, (match, arr) => { return `countRange(${arr})`; }); expr = expr.replace(/MAX\s*\(\s*(\[.*?\])\s*\)/gi, (match, arr) => { return `maxRange(${arr})`; }); expr = expr.replace(/MIN\s*\(\s*(\[.*?\])\s*\)/gi, (match, arr) => { return `minRange(${arr})`; }); expr = expr.replace( /IF\s*\(\s*(.*?)\s*,\s*(.*?)\s*,\s*(.*?)\s*\)/gi, (match, cond, t, f) => { return `(${cond} ? ${t} : ${f})`; }, ); const result = new Function( "sumRange", "averageRange", "countRange", "maxRange", "minRange", `return ${expr}`, )(sumRange, averageRange, countRange, maxRange, minRange); return isNaN(result) ? result : Math.round(result * 1000000) / 1000000; } catch (e) { return "#ERROR!"; } } function parseRange(startRef, endRef) { const start = parseCellRef(startRef); const end = parseCellRef(endRef); if (!start || !end) return []; const values = []; for (let r = start.row; r <= end.row; r++) { for (let c = start.col; c <= end.col; c++) { const val = getCellValue(r, c); const num = parseFloat(val); if (!isNaN(num)) values.push(num); } } return values; } function sumRange(values) { return values.reduce((a, b) => a + b, 0); } function averageRange(values) { if (values.length === 0) return 0; return sumRange(values) / values.length; } function countRange(values) { return values.length; } function maxRange(values) { return values.length > 0 ? Math.max(...values) : 0; } function minRange(values) { return values.length > 0 ? Math.min(...values) : 0; } // ============================================================================= // KEYBOARD HANDLING // ============================================================================= function handleKeyDown(e) { if (state.isEditing) return; const { row, col } = state.activeCell; if ((e.ctrlKey || e.metaKey) && !e.shiftKey) { switch (e.key.toLowerCase()) { case "c": e.preventDefault(); copySelection(); break; case "x": e.preventDefault(); cutSelection(); break; case "v": e.preventDefault(); pasteSelection(); break; case "z": e.preventDefault(); undo(); break; case "y": e.preventDefault(); redo(); break; case "b": e.preventDefault(); formatCells("bold"); break; case "i": e.preventDefault(); formatCells("italic"); break; case "u": e.preventDefault(); formatCells("underline"); break; case "s": e.preventDefault(); saveSheet(); break; case "a": e.preventDefault(); selectAll(); break; } return; } switch (e.key) { case "ArrowUp": e.preventDefault(); if (e.shiftKey) { extendSelection( Math.max(0, state.selection.end.row - 1), state.selection.end.col, ); } else { navigateCell(-1, 0); } break; case "ArrowDown": e.preventDefault(); if (e.shiftKey) { extendSelection( Math.min(CONFIG.ROWS - 1, state.selection.end.row + 1), state.selection.end.col, ); } else { navigateCell(1, 0); } break; case "ArrowLeft": e.preventDefault(); if (e.shiftKey) { extendSelection( state.selection.end.row, Math.max(0, state.selection.end.col - 1), ); } else { navigateCell(0, -1); } break; case "ArrowRight": e.preventDefault(); if (e.shiftKey) { extendSelection( state.selection.end.row, Math.min(CONFIG.COLS - 1, state.selection.end.col + 1), ); } else { navigateCell(0, 1); } break; case "Enter": e.preventDefault(); startEditing(row, col); break; case "Tab": e.preventDefault(); navigateCell(0, e.shiftKey ? -1 : 1); break; case "Delete": case "Backspace": e.preventDefault(); clearCells(); break; case "F2": e.preventDefault(); startEditing(row, col); break; default: if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { startEditing(row, col); const cell = document.getElementById(`cell-${row}-${col}`); const input = cell ? cell.querySelector(".cell-input") : null; if (input) { input.value = e.key; } } } } function navigateCell(deltaRow, deltaCol) { const newRow = Math.max( 0, Math.min(CONFIG.ROWS - 1, state.activeCell.row + deltaRow), ); const newCol = Math.max( 0, Math.min(CONFIG.COLS - 1, state.activeCell.col + deltaCol), ); selectCell(newRow, newCol); scrollCellIntoView(newRow, newCol); } function scrollCellIntoView(row, col) { const cell = document.getElementById(`cell-${row}-${col}`); if (cell) { cell.scrollIntoView({ block: "nearest", inline: "nearest" }); } } function selectAll() { state.selection = { start: { row: 0, col: 0 }, end: { row: CONFIG.ROWS - 1, col: CONFIG.COLS - 1 }, }; clearSelection(); for (let r = 0; r < CONFIG.ROWS; r++) { for (let c = 0; c < CONFIG.COLS; c++) { const cell = document.getElementById(`cell-${r}-${c}`); if (cell) { cell.classList.add(r === 0 && c === 0 ? "selected" : "in-range"); } } } } // ============================================================================= // FORMULA BAR // ============================================================================= function handleFormulaKey(e) { if (e.key === "Enter") { e.preventDefault(); const value = elements.formulaInput.value; const { row, col } = state.activeCell; setCellValue(row, col, value); renderCell(row, col); elements.formulaInput.blur(); } else if (e.key === "Escape") { updateFormulaBar(); elements.formulaInput.blur(); } } function updateFormulaPreview() { if (!elements.formulaInput || !elements.formulaPreview) return; const value = elements.formulaInput.value; if (value.startsWith("=")) { const result = evaluateFormula( value, state.activeCell.row, state.activeCell.col, ); elements.formulaPreview.textContent = `= ${result}`; } else { elements.formulaPreview.textContent = ""; } } function updateCellAddress() { if (!elements.cellAddress) return; const ref = getCellRef(state.activeCell.row, state.activeCell.col); elements.cellAddress.textContent = ref; } function updateFormulaBar() { if (!elements.formulaInput) return; const data = getCellData(state.activeCell.row, state.activeCell.col); elements.formulaInput.value = data ? data.value || "" : ""; updateFormulaPreview(); } function updateSelectionInfo() { const info = document.getElementById("selection-info"); if (!info) return; const { start, end } = state.selection; const rows = end.row - start.row + 1; const cols = end.col - start.col + 1; const count = rows * cols; if (count > 1) { info.textContent = `${rows}R × ${cols}C`; } else { info.textContent = ""; } } function updateCalculationResult() { const result = document.getElementById("calculation-result"); if (!result) return; const { start, end } = state.selection; const values = []; for (let r = start.row; r <= end.row; r++) { for (let c = start.col; c <= end.col; c++) { const val = getCellValue(r, c); const num = parseFloat(val); if (!isNaN(num)) values.push(num); } } if (values.length > 1) { const sum = values.reduce((a, b) => a + b, 0); const avg = sum / values.length; result.textContent = `Sum: ${sum.toFixed(2)} | Avg: ${avg.toFixed(2)} | Count: ${values.length}`; } else { result.textContent = ""; } } // ============================================================================= // CLIPBOARD OPERATIONS // ============================================================================= function copySelection() { state.clipboard = getSelectionData(); state.clipboardMode = "copy"; showCopyBox(); } function cutSelection() { state.clipboard = getSelectionData(); state.clipboardMode = "cut"; showCopyBox(); } function pasteSelection() { if (!state.clipboard) return; const { row, col } = state.activeCell; const data = state.clipboard; saveToHistory(); for (let r = 0; r < data.length; r++) { for (let c = 0; c < data[r].length; c++) { const targetRow = row + r; const targetCol = col + c; if (targetRow < CONFIG.ROWS && targetCol < CONFIG.COLS) { const cellData = data[r][c]; if (cellData) { const ws = state.worksheets[state.activeWorksheet]; ws.data[`${targetRow},${targetCol}`] = { ...cellData }; } renderCell(targetRow, targetCol); } } } if (state.clipboardMode === "cut") { clearSourceCells(); state.clipboard = null; state.clipboardMode = null; hideCopyBox(); } state.isDirty = true; scheduleAutoSave(); } function getSelectionData() { const { start, end } = state.selection; const data = []; for (let r = start.row; r <= end.row; r++) { const rowData = []; for (let c = start.col; c <= end.col; c++) { rowData.push(getCellData(r, c) ? { ...getCellData(r, c) } : null); } data.push(rowData); } return data; } function clearSourceCells() { const { start, end } = state.selection; for (let r = start.row; r <= end.row; r++) { for (let c = start.col; c <= end.col; c++) { setCellValue(r, c, ""); renderCell(r, c); } } } function clearCells() { const { start, end } = state.selection; saveToHistory(); for (let r = start.row; r <= end.row; r++) { for (let c = start.col; c <= end.col; c++) { setCellValue(r, c, ""); renderCell(r, c); } } } function showCopyBox() { const box = document.getElementById("copy-box"); if (!box) return; const { start, end } = state.selection; const startCell = document.getElementById(`cell-${start.row}-${start.col}`); const endCell = document.getElementById(`cell-${end.row}-${end.col}`); if (!startCell || !endCell || !elements.cellsContainer) return; const containerRect = elements.cellsContainer.getBoundingClientRect(); const startRect = startCell.getBoundingClientRect(); const endRect = endCell.getBoundingClientRect(); box.style.left = startRect.left - containerRect.left + "px"; box.style.top = startRect.top - containerRect.top + "px"; box.style.width = endRect.right - startRect.left + "px"; box.style.height = endRect.bottom - startRect.top + "px"; box.classList.remove("hidden"); } function hideCopyBox() { const box = document.getElementById("copy-box"); if (box) box.classList.add("hidden"); } // ============================================================================= // FORMATTING // ============================================================================= function formatCells(format, value) { const { start, end } = state.selection; const ws = state.worksheets[state.activeWorksheet]; saveToHistory(); for (let r = start.row; r <= end.row; r++) { for (let c = start.col; c <= end.col; c++) { const key = `${r},${c}`; if (!ws.data[key]) { ws.data[key] = { value: "", format: {} }; } if (!ws.data[key].format) { ws.data[key].format = {}; } switch (format) { case "bold": ws.data[key].format.bold = !ws.data[key].format.bold; break; case "italic": ws.data[key].format.italic = !ws.data[key].format.italic; break; case "underline": ws.data[key].format.underline = !ws.data[key].format.underline; break; case "strikethrough": ws.data[key].format.strikethrough = !ws.data[key].format.strikethrough; break; case "fontFamily": ws.data[key].format.fontFamily = value; break; case "fontSize": ws.data[key].format.fontSize = value; break; case "color": ws.data[key].format.color = value; break; case "backgroundColor": ws.data[key].format.backgroundColor = value; break; case "alignLeft": ws.data[key].format.textAlign = "left"; break; case "alignCenter": ws.data[key].format.textAlign = "center"; break; case "alignRight": ws.data[key].format.textAlign = "right"; break; } renderCell(r, c); } } state.isDirty = true; scheduleAutoSave(); } // ============================================================================= // HISTORY (UNDO/REDO) // ============================================================================= function saveToHistory() { const snapshot = JSON.stringify(state.worksheets); state.history = state.history.slice(0, state.historyIndex + 1); state.history.push(snapshot); if (state.history.length > CONFIG.MAX_HISTORY) { state.history.shift(); } state.historyIndex = state.history.length - 1; } function undo() { if (state.historyIndex > 0) { state.historyIndex--; state.worksheets = JSON.parse(state.history[state.historyIndex]); renderAllCells(); state.isDirty = true; } } function redo() { if (state.historyIndex < state.history.length - 1) { state.historyIndex++; state.worksheets = JSON.parse(state.history[state.historyIndex]); renderAllCells(); state.isDirty = true; } } // ============================================================================= // CONTEXT MENU // ============================================================================= function handleContextMenu(e) { e.preventDefault(); const menu = elements.contextMenu; if (!menu) return; menu.style.left = e.clientX + "px"; menu.style.top = e.clientY + "px"; menu.classList.remove("hidden"); } function hideContextMenus() { if (elements.contextMenu) elements.contextMenu.classList.add("hidden"); if (elements.tabContextMenu) elements.tabContextMenu.classList.add("hidden"); } // ============================================================================= // ROW/COLUMN OPERATIONS // ============================================================================= function insertRow() { const row = state.activeCell.row; const ws = state.worksheets[state.activeWorksheet]; const newData = {}; saveToHistory(); for (const key in ws.data) { const [r, c] = key.split(",").map(Number); if (r >= row) { newData[`${r + 1},${c}`] = ws.data[key]; } else { newData[key] = ws.data[key]; } } ws.data = newData; renderAllCells(); state.isDirty = true; scheduleAutoSave(); } function insertColumn() { const col = state.activeCell.col; const ws = state.worksheets[state.activeWorksheet]; const newData = {}; saveToHistory(); for (const key in ws.data) { const [r, c] = key.split(",").map(Number); if (c >= col) { newData[`${r},${c + 1}`] = ws.data[key]; } else { newData[key] = ws.data[key]; } } ws.data = newData; renderAllCells(); state.isDirty = true; scheduleAutoSave(); } function deleteRow() { const row = state.activeCell.row; const ws = state.worksheets[state.activeWorksheet]; const newData = {}; saveToHistory(); for (const key in ws.data) { const [r, c] = key.split(",").map(Number); if (r < row) { newData[key] = ws.data[key]; } else if (r > row) { newData[`${r - 1},${c}`] = ws.data[key]; } } ws.data = newData; renderAllCells(); state.isDirty = true; scheduleAutoSave(); } function deleteColumn() { const col = state.activeCell.col; const ws = state.worksheets[state.activeWorksheet]; const newData = {}; saveToHistory(); for (const key in ws.data) { const [r, c] = key.split(",").map(Number); if (c < col) { newData[key] = ws.data[key]; } else if (c > col) { newData[`${r},${c - 1}`] = ws.data[key]; } } ws.data = newData; renderAllCells(); state.isDirty = true; scheduleAutoSave(); } // ============================================================================= // SORTING // ============================================================================= function sortAscending() { sortSelection(true); } function sortDescending() { sortSelection(false); } function sortSelection(ascending) { const { start, end } = state.selection; const ws = state.worksheets[state.activeWorksheet]; const rows = []; saveToHistory(); for (let r = start.row; r <= end.row; r++) { const rowData = []; for (let c = start.col; c <= end.col; c++) { rowData.push(getCellData(r, c)); } rows.push({ row: r, data: rowData }); } rows.sort((a, b) => { const valA = a.data[0]?.value || ""; const valB = b.data[0]?.value || ""; const numA = parseFloat(valA); const numB = parseFloat(valB); if (!isNaN(numA) && !isNaN(numB)) { return ascending ? numA - numB : numB - numA; } return ascending ? valA.localeCompare(valB) : valB.localeCompare(valA); }); rows.forEach((rowObj, i) => { const targetRow = start.row + i; rowObj.data.forEach((cellData, j) => { const targetCol = start.col + j; const key = `${targetRow},${targetCol}`; if (cellData) { ws.data[key] = cellData; } else { delete ws.data[key]; } }); }); renderAllCells(); state.isDirty = true; scheduleAutoSave(); } // ============================================================================= // WORKSHEETS // ============================================================================= function addWorksheet() { const num = state.worksheets.length + 1; state.worksheets.push({ name: `Sheet${num}`, data: {} }); state.activeWorksheet = state.worksheets.length - 1; renderWorksheetTabs(); renderAllCells(); selectCell(0, 0); } function switchWorksheet(index) { if (index < 0 || index >= state.worksheets.length) return; state.activeWorksheet = index; renderWorksheetTabs(); renderAllCells(); selectCell(0, 0); } function renderWorksheetTabs() { if (!elements.worksheetTabs) return; elements.worksheetTabs.innerHTML = state.worksheets .map( (ws, i) => `