/* ============================================================================= GB SHEET - Modern Spreadsheet with AI Chat ============================================================================= */ (function () { "use strict"; const CONFIG = { COLS: 26, ROWS: 100, COL_WIDTH: 100, ROW_HEIGHT: 24, MAX_HISTORY: 50, AUTOSAVE_DELAY: 3000, WS_RECONNECT_DELAY: 3000, }; 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: 100, collaborators: [], ws: null, isEditing: false, isSelecting: false, isDirty: false, autoSaveTimer: null, chatPanelOpen: true, }; const elements = {}; function init() { cacheElements(); renderGrid(); bindEvents(); loadFromUrlParams(); connectWebSocket(); connectChatWebSocket(); selectCell(0, 0); updateCellAddress(); } function cacheElements() { elements.app = document.getElementById("sheet-app"); elements.sheetName = document.getElementById("sheetName"); elements.columnHeaders = document.getElementById("columnHeaders"); elements.rowHeaders = document.getElementById("rowHeaders"); elements.cells = document.getElementById("cells"); elements.cellsContainer = document.getElementById("cellsContainer"); elements.formulaInput = document.getElementById("formulaInput"); elements.cellAddress = document.getElementById("cellAddress"); elements.worksheetTabs = document.getElementById("worksheetTabs"); elements.collaborators = document.getElementById("collaborators"); elements.contextMenu = document.getElementById("contextMenu"); elements.shareModal = document.getElementById("shareModal"); elements.chartModal = document.getElementById("chartModal"); elements.cursorIndicators = document.getElementById("cursorIndicators"); elements.selectionBox = document.getElementById("selectionBox"); elements.selectionInfo = document.getElementById("selectionInfo"); elements.calculationResult = document.getElementById("calculationResult"); elements.saveStatus = document.getElementById("saveStatus"); elements.zoomLevel = document.getElementById("zoomLevel"); elements.chatPanel = document.getElementById("chatPanel"); elements.chatMessages = document.getElementById("chatMessages"); elements.chatInput = document.getElementById("chatInput"); elements.chatForm = document.getElementById("chatForm"); } function renderGrid() { elements.columnHeaders.innerHTML = ""; for (let col = 0; col < CONFIG.COLS; col++) { const header = document.createElement("div"); header.className = "column-header"; header.textContent = getColName(col); header.dataset.col = col; elements.columnHeaders.appendChild(header); } elements.rowHeaders.innerHTML = ""; for (let row = 0; row < CONFIG.ROWS; row++) { const header = document.createElement("div"); header.className = "row-header"; header.textContent = row + 1; header.dataset.row = row; elements.rowHeaders.appendChild(header); } elements.cells.innerHTML = ""; elements.cells.style.gridTemplateColumns = `repeat(${CONFIG.COLS}, ${CONFIG.COL_WIDTH}px)`; for (let row = 0; row < CONFIG.ROWS; row++) { for (let col = 0; col < CONFIG.COLS; col++) { const cell = document.createElement("div"); cell.className = "cell"; cell.dataset.row = row; cell.dataset.col = col; elements.cells.appendChild(cell); } } renderAllCells(); } function renderAllCells() { const ws = state.worksheets[state.activeWorksheet]; if (!ws) return; const cells = elements.cells.querySelectorAll(".cell"); cells.forEach((cell) => { const row = parseInt(cell.dataset.row); const col = parseInt(cell.dataset.col); renderCell(row, col); }); } function renderCell(row, col) { const cell = elements.cells.querySelector( `[data-row="${row}"][data-col="${col}"]`, ); if (!cell) return; const data = getCellData(row, col); let displayValue = ""; if (data) { if (data.formula) { displayValue = evaluateFormula(data.formula, row, col); } else if (data.value !== undefined) { displayValue = data.value; } applyFormatToCell(cell, data.style); } else { cell.style.cssText = ""; } cell.textContent = displayValue; } function applyFormatToCell(cell, style) { if (!style) return; if (style.fontFamily) cell.style.fontFamily = style.fontFamily; if (style.fontSize) cell.style.fontSize = style.fontSize + "px"; if (style.fontWeight) cell.style.fontWeight = style.fontWeight; if (style.fontStyle) cell.style.fontStyle = style.fontStyle; if (style.textDecoration) cell.style.textDecoration = style.textDecoration; if (style.color) cell.style.color = style.color; if (style.background) cell.style.backgroundColor = style.background; if (style.textAlign) cell.style.textAlign = style.textAlign; } 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()), }; } function bindEvents() { elements.cells.addEventListener("mousedown", handleCellMouseDown); elements.cells.addEventListener("dblclick", handleCellDoubleClick); document.addEventListener("mousemove", handleMouseMove); document.addEventListener("mouseup", handleMouseUp); document.addEventListener("keydown", handleKeyDown); document.addEventListener("click", handleDocumentClick); document.addEventListener("contextmenu", handleContextMenu); elements.columnHeaders.addEventListener("click", handleColumnHeaderClick); elements.rowHeaders.addEventListener("click", handleRowHeaderClick); elements.formulaInput.addEventListener("keydown", handleFormulaKey); elements.formulaInput.addEventListener("input", updateFormulaPreview); document.getElementById("undoBtn")?.addEventListener("click", undo); document.getElementById("redoBtn")?.addEventListener("click", redo); document .getElementById("boldBtn") ?.addEventListener("click", () => formatCells("bold")); document .getElementById("italicBtn") ?.addEventListener("click", () => formatCells("italic")); document .getElementById("underlineBtn") ?.addEventListener("click", () => formatCells("underline")); document .getElementById("strikeBtn") ?.addEventListener("click", () => formatCells("strikethrough")); document .getElementById("alignLeftBtn") ?.addEventListener("click", () => formatCells("alignLeft")); document .getElementById("alignCenterBtn") ?.addEventListener("click", () => formatCells("alignCenter")); document .getElementById("alignRightBtn") ?.addEventListener("click", () => formatCells("alignRight")); document .getElementById("mergeCellsBtn") ?.addEventListener("click", mergeCells); document .getElementById("formatCurrencyBtn") ?.addEventListener("click", () => formatCells("currency")); document .getElementById("formatPercentBtn") ?.addEventListener("click", () => formatCells("percent")); document .getElementById("textColorInput") ?.addEventListener("input", (e) => { formatCells("color", e.target.value); document.getElementById("textColorIndicator").style.background = e.target.value; }); document.getElementById("bgColorInput")?.addEventListener("input", (e) => { formatCells("backgroundColor", e.target.value); document.getElementById("bgColorIndicator").style.background = e.target.value; }); document .getElementById("fontFamily") ?.addEventListener("change", (e) => formatCells("fontFamily", e.target.value), ); document .getElementById("fontSize") ?.addEventListener("change", (e) => formatCells("fontSize", e.target.value), ); document .getElementById("shareBtn") ?.addEventListener("click", showShareModal); document .getElementById("closeShareModal") ?.addEventListener("click", () => hideModal("shareModal")); document .getElementById("closeChartModal") ?.addEventListener("click", () => hideModal("chartModal")); document .getElementById("copyLinkBtn") ?.addEventListener("click", copyShareLink); document .getElementById("addSheetBtn") ?.addEventListener("click", addWorksheet); document.getElementById("zoomInBtn")?.addEventListener("click", zoomIn); document.getElementById("zoomOutBtn")?.addEventListener("click", zoomOut); document .getElementById("chatToggle") ?.addEventListener("click", toggleChatPanel); document .getElementById("chatClose") ?.addEventListener("click", toggleChatPanel); elements.chatForm?.addEventListener("submit", handleChatSubmit); document.querySelectorAll(".suggestion-btn").forEach((btn) => { btn.addEventListener("click", () => handleSuggestionClick(btn.dataset.action), ); }); document.querySelectorAll(".context-item").forEach((item) => { item.addEventListener("click", () => handleContextAction(item.dataset.action), ); }); elements.sheetName?.addEventListener("change", (e) => { state.sheetName = e.target.value; scheduleAutoSave(); }); window.addEventListener("beforeunload", handleBeforeUnload); } 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 (state.isEditing) { finishEditing(); } if (e.shiftKey) { extendSelection(row, col); } else { selectCell(row, col); state.isSelecting = true; } } function handleMouseMove(e) { if (!state.isSelecting) return; const cell = document .elementFromPoint(e.clientX, e.clientY) ?.closest(".cell"); if (cell) { const row = parseInt(cell.dataset.row); const col = parseInt(cell.dataset.col); extendSelection(row, col); } } function handleMouseUp() { state.isSelecting = false; } 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 = elements.cells.querySelector( `[data-row="${row}"][data-col="${col}"]`, ); if (cell) { cell.classList.add("selected"); cell.scrollIntoView({ block: "nearest", inline: "nearest" }); } updateCellAddress(); updateFormulaBar(); updateSelectionInfo(); } function extendSelection(row, col) { clearSelection(); const start = state.activeCell; 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), }, }; 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 = elements.cells.querySelector( `[data-row="${r}"][data-col="${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() { elements.cells .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); clearSelection(); state.activeCell = { row: 0, col }; state.selection = { start: { row: 0, col }, end: { row: CONFIG.ROWS - 1, col }, }; for (let row = 0; row < CONFIG.ROWS; row++) { const cell = elements.cells.querySelector( `[data-row="${row}"][data-col="${col}"]`, ); if (cell) cell.classList.add("in-range"); } header.classList.add("selected"); updateSelectionInfo(); } function handleRowHeaderClick(e) { const header = e.target.closest(".row-header"); if (!header) return; const row = parseInt(header.dataset.row); clearSelection(); state.activeCell = { row, col: 0 }; state.selection = { start: { row, col: 0 }, end: { row, col: CONFIG.COLS - 1 }, }; for (let col = 0; col < CONFIG.COLS; col++) { const cell = elements.cells.querySelector( `[data-row="${row}"][data-col="${col}"]`, ); if (cell) cell.classList.add("in-range"); } header.classList.add("selected"); updateSelectionInfo(); } function startEditing(row, col) { const cell = elements.cells.querySelector( `[data-row="${row}"][data-col="${col}"]`, ); if (!cell) return; state.isEditing = true; const data = getCellData(row, col); const input = document.createElement("input"); input.type = "text"; input.className = "cell-input"; input.value = data?.formula || data?.value || ""; cell.textContent = ""; cell.classList.add("editing"); cell.appendChild(input); input.focus(); input.select(); input.addEventListener("keydown", (e) => { if (e.key === "Enter") { finishEditing(true); navigateCell(1, 0); } else if (e.key === "Tab") { e.preventDefault(); finishEditing(true); navigateCell(0, e.shiftKey ? -1 : 1); } else if (e.key === "Escape") { cancelEditing(); } }); input.addEventListener("blur", () => { if (state.isEditing) finishEditing(true); }); } function finishEditing(save = true) { if (!state.isEditing) return; const { row, col } = state.activeCell; const cell = elements.cells.querySelector( `[data-row="${row}"][data-col="${col}"]`, ); const input = cell?.querySelector(".cell-input"); if (input && save) { const value = input.value.trim(); setCellValue(row, col, value); } state.isEditing = false; cell?.classList.remove("editing"); renderCell(row, col); updateFormulaBar(); } function cancelEditing() { state.isEditing = false; const { row, col } = state.activeCell; const cell = elements.cells.querySelector( `[data-row="${row}"][data-col="${col}"]`, ); cell?.classList.remove("editing"); renderCell(row, col); } function setCellValue(row, col, value) { const ws = state.worksheets[state.activeWorksheet]; const key = `${row},${col}`; saveToHistory(); if (!value) { delete ws.data[key]; } else if (value.startsWith("=")) { ws.data[key] = { formula: value }; } else { ws.data[key] = { value }; } state.isDirty = true; scheduleAutoSave(); broadcastChange("cell", { row, col, value }); } function getCellData(row, col) { const ws = state.worksheets[state.activeWorksheet]; return ws?.data[`${row},${col}`]; } function getCellValue(row, col) { const data = getCellData(row, col); if (!data) return ""; if (data.formula) return evaluateFormula(data.formula, row, col); return data.value || ""; } function evaluateFormula(formula, sourceRow, sourceCol) { if (!formula.startsWith("=")) return formula; try { let expr = formula.substring(1).toUpperCase(); expr = expr.replace(/([A-Z]+)(\d+)/g, (match, col, row) => { const r = parseInt(row) - 1; const c = parseColName(col); const val = getCellValue(r, c); const num = parseFloat(val); return isNaN(num) ? `"${val}"` : num; }); if (expr.startsWith("SUM(")) { return evaluateSum(expr); } else if (expr.startsWith("AVERAGE(")) { return evaluateAverage(expr); } else if (expr.startsWith("COUNT(")) { return evaluateCount(expr); } else if (expr.startsWith("MAX(")) { return evaluateMax(expr); } else if (expr.startsWith("MIN(")) { return evaluateMin(expr); } else if (expr.startsWith("IF(")) { return evaluateIf(expr); } const result = new Function("return " + expr)(); return typeof result === "number" ? Math.round(result * 1000000) / 1000000 : result; } catch (e) { return "#ERROR"; } } function evaluateSum(expr) { const match = expr.match(/SUM\(([^)]+)\)/i); if (!match) return "#ERROR"; const values = parseRange(match[1]); return values.reduce((a, b) => a + b, 0); } function evaluateAverage(expr) { const match = expr.match(/AVERAGE\(([^)]+)\)/i); if (!match) return "#ERROR"; const values = parseRange(match[1]); return values.length ? values.reduce((a, b) => a + b, 0) / values.length : 0; } function evaluateCount(expr) { const match = expr.match(/COUNT\(([^)]+)\)/i); if (!match) return "#ERROR"; const values = parseRange(match[1]); return values.length; } function evaluateMax(expr) { const match = expr.match(/MAX\(([^)]+)\)/i); if (!match) return "#ERROR"; const values = parseRange(match[1]); return values.length ? Math.max(...values) : 0; } function evaluateMin(expr) { const match = expr.match(/MIN\(([^)]+)\)/i); if (!match) return "#ERROR"; const values = parseRange(match[1]); return values.length ? Math.min(...values) : 0; } function evaluateIf(expr) { const match = expr.match(/IF\(([^,]+),([^,]+),([^)]+)\)/i); if (!match) return "#ERROR"; try { const condition = new Function("return " + match[1])(); return condition ? new Function("return " + match[2])() : new Function("return " + match[3])(); } catch { return "#ERROR"; } } function parseRange(rangeStr) { const values = []; const parts = rangeStr.split(":"); if (parts.length === 2) { const start = parseCellRef(parts[0].trim()); const end = parseCellRef(parts[1].trim()); if (start && end) { for (let r = start.row; r <= end.row; r++) { for (let c = start.col; c <= end.col; c++) { const val = parseFloat(getCellValue(r, c)); if (!isNaN(val)) values.push(val); } } } } else { const ref = parseCellRef(parts[0].trim()); if (ref) { const val = parseFloat(getCellValue(ref.row, ref.col)); if (!isNaN(val)) values.push(val); } } return values; } function handleKeyDown(e) { if (e.target.closest(".chat-input, .modal input, .sheet-name-input")) return; const { row, col } = state.activeCell; if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case "c": copySelection(); return; case "x": cutSelection(); return; case "v": pasteSelection(); return; case "z": e.shiftKey ? redo() : undo(); e.preventDefault(); return; case "y": redo(); e.preventDefault(); return; case "b": formatCells("bold"); e.preventDefault(); return; case "i": formatCells("italic"); e.preventDefault(); return; case "u": formatCells("underline"); e.preventDefault(); return; case "a": selectAll(); e.preventDefault(); return; } } if (state.isEditing) return; switch (e.key) { case "ArrowUp": navigateCell(-1, 0); e.preventDefault(); break; case "ArrowDown": navigateCell(1, 0); e.preventDefault(); break; case "ArrowLeft": navigateCell(0, -1); e.preventDefault(); break; case "ArrowRight": navigateCell(0, 1); e.preventDefault(); break; case "Tab": navigateCell(0, e.shiftKey ? -1 : 1); e.preventDefault(); break; case "Enter": if (e.shiftKey) { navigateCell(-1, 0); } else { startEditing(row, col); } e.preventDefault(); break; case "Delete": case "Backspace": clearCells(); e.preventDefault(); break; case "F2": startEditing(row, col); e.preventDefault(); break; default: if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) { startEditing(row, col); const cell = elements.cells.querySelector( `[data-row="${row}"][data-col="${col}"]`, ); const input = cell?.querySelector(".cell-input"); if (input) input.value = e.key; } } } function navigateCell(dRow, dCol) { const newRow = Math.max( 0, Math.min(CONFIG.ROWS - 1, state.activeCell.row + dRow), ); const newCol = Math.max( 0, Math.min(CONFIG.COLS - 1, state.activeCell.col + dCol), ); selectCell(newRow, newCol); } function selectAll() { clearSelection(); state.selection = { start: { row: 0, col: 0 }, end: { row: CONFIG.ROWS - 1, col: CONFIG.COLS - 1 }, }; elements.cells.querySelectorAll(".cell").forEach((cell) => { cell.classList.add("in-range"); }); const activeCell = elements.cells.querySelector( `[data-row="${state.activeCell.row}"][data-col="${state.activeCell.col}"]`, ); if (activeCell) { activeCell.classList.remove("in-range"); activeCell.classList.add("selected"); } updateSelectionInfo(); } 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() { const value = elements.formulaInput.value; if (value.startsWith("=")) { const result = evaluateFormula( value, state.activeCell.row, state.activeCell.col, ); elements.calculationResult.textContent = `= ${result}`; } else { elements.calculationResult.textContent = ""; } } function updateCellAddress() { const ref = getCellRef(state.activeCell.row, state.activeCell.col); elements.cellAddress.textContent = ref; } function updateFormulaBar() { const data = getCellData(state.activeCell.row, state.activeCell.col); elements.formulaInput.value = data?.formula || data?.value || ""; } function updateSelectionInfo() { 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) { elements.selectionInfo.textContent = "Ready"; } else { elements.selectionInfo.textContent = `${rows}R × ${cols}C = ${count} cells`; } } function updateCalculationResult() { 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 = parseFloat(getCellValue(r, c)); if (!isNaN(val)) values.push(val); } } if (values.length > 1) { const sum = values.reduce((a, b) => a + b, 0); const avg = sum / values.length; elements.calculationResult.textContent = `Sum: ${sum.toFixed(2)} | Avg: ${avg.toFixed(2)} | Count: ${values.length}`; } else { elements.calculationResult.textContent = ""; } } function copySelection() { state.clipboard = getSelectionData(); state.clipboardMode = "copy"; showCopyBox(); } function cutSelection() { state.clipboard = getSelectionData(); state.clipboardMode = "cut"; showCopyBox(); } function pasteSelection() { if (!state.clipboard) return; saveToHistory(); const { row, col } = state.activeCell; const ws = state.worksheets[state.activeWorksheet]; state.clipboard.forEach((rowData, rOffset) => { rowData.forEach((cellData, cOffset) => { const targetRow = row + rOffset; const targetCol = col + cOffset; const key = `${targetRow},${targetCol}`; if (cellData) { ws.data[key] = { ...cellData }; } renderCell(targetRow, targetCol); }); }); if (state.clipboardMode === "cut") { clearSourceCells(); 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) || null); } data.push(rowData); } return data; } function clearSourceCells() { const { start, end } = state.selection; const ws = state.worksheets[state.activeWorksheet]; for (let r = start.row; r <= end.row; r++) { for (let c = start.col; c <= end.col; c++) { delete ws.data[`${r},${c}`]; renderCell(r, c); } } } function clearCells() { saveToHistory(); const { start, end } = state.selection; const ws = state.worksheets[state.activeWorksheet]; for (let r = start.row; r <= end.row; r++) { for (let c = start.col; c <= end.col; c++) { delete ws.data[`${r},${c}`]; renderCell(r, c); } } state.isDirty = true; scheduleAutoSave(); } function showCopyBox() { const copyBox = document.getElementById("copyBox"); if (copyBox) copyBox.classList.remove("hidden"); } function hideCopyBox() { const copyBox = document.getElementById("copyBox"); if (copyBox) copyBox.classList.add("hidden"); } function formatCells(format, value) { saveToHistory(); const { start, end } = state.selection; const ws = state.worksheets[state.activeWorksheet]; 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: "" }; if (!ws.data[key].style) ws.data[key].style = {}; const style = ws.data[key].style; switch (format) { case "bold": style.fontWeight = style.fontWeight === "bold" ? "normal" : "bold"; break; case "italic": style.fontStyle = style.fontStyle === "italic" ? "normal" : "italic"; break; case "underline": style.textDecoration = style.textDecoration === "underline" ? "none" : "underline"; break; case "strikethrough": style.textDecoration = style.textDecoration === "line-through" ? "none" : "line-through"; break; case "alignLeft": style.textAlign = "left"; break; case "alignCenter": style.textAlign = "center"; break; case "alignRight": style.textAlign = "right"; break; case "fontFamily": style.fontFamily = value; break; case "fontSize": style.fontSize = value; break; case "color": style.color = value; break; case "backgroundColor": style.background = value; break; case "currency": if (ws.data[key].value) { const num = parseFloat(ws.data[key].value); if (!isNaN(num)) ws.data[key].value = "$" + num.toFixed(2); } break; case "percent": if (ws.data[key].value) { const num = parseFloat(ws.data[key].value); if (!isNaN(num)) ws.data[key].value = (num * 100).toFixed(0) + "%"; } break; } renderCell(r, c); } } state.isDirty = true; scheduleAutoSave(); } function mergeCells() { addChatMessage("assistant", "Merge cells feature coming soon!"); } 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; } } function handleContextMenu(e) { const cell = e.target.closest(".cell"); if (!cell) return; e.preventDefault(); elements.contextMenu.style.left = e.clientX + "px"; elements.contextMenu.style.top = e.clientY + "px"; elements.contextMenu.classList.remove("hidden"); } function handleDocumentClick(e) { if (!e.target.closest(".context-menu")) { elements.contextMenu?.classList.add("hidden"); } } function handleContextAction(action) { elements.contextMenu.classList.add("hidden"); switch (action) { case "cut": cutSelection(); break; case "copy": copySelection(); break; case "paste": pasteSelection(); break; case "insertRowAbove": insertRow(state.activeCell.row); break; case "insertRowBelow": insertRow(state.activeCell.row + 1); break; case "insertColLeft": insertColumn(state.activeCell.col); break; case "insertColRight": insertColumn(state.activeCell.col + 1); break; case "deleteRow": deleteRow(state.activeCell.row); break; case "deleteCol": deleteColumn(state.activeCell.col); break; case "clearContents": clearCells(); break; case "clearFormatting": clearFormatting(); break; } } function insertRow(atRow) { saveToHistory(); const ws = state.worksheets[state.activeWorksheet]; const newData = {}; for (const key in ws.data) { const [r, c] = key.split(",").map(Number); if (r >= atRow) { newData[`${r + 1},${c}`] = ws.data[key]; } else { newData[key] = ws.data[key]; } } ws.data = newData; renderAllCells(); state.isDirty = true; scheduleAutoSave(); } function insertColumn(atCol) { saveToHistory(); const ws = state.worksheets[state.activeWorksheet]; const newData = {}; for (const key in ws.data) { const [r, c] = key.split(",").map(Number); if (c >= atCol) { newData[`${r},${c + 1}`] = ws.data[key]; } else { newData[key] = ws.data[key]; } } ws.data = newData; renderAllCells(); state.isDirty = true; scheduleAutoSave(); } function deleteRow(row) { saveToHistory(); const ws = state.worksheets[state.activeWorksheet]; const newData = {}; 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(col) { saveToHistory(); const ws = state.worksheets[state.activeWorksheet]; const newData = {}; 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(); } function clearFormatting() { const { start, end } = state.selection; const ws = state.worksheets[state.activeWorksheet]; 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]) { delete ws.data[key].style; renderCell(r, c); } } } state.isDirty = true; scheduleAutoSave(); } 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); state.isDirty = true; scheduleAutoSave(); } function switchWorksheet(index) { if (index < 0 || index >= state.worksheets.length) return; state.activeWorksheet = index; renderWorksheetTabs(); renderAllCells(); selectCell(0, 0); } function renderWorksheetTabs() { elements.worksheetTabs.innerHTML = state.worksheets .map( (ws, i) => `