/* ============================================================================= 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, findMatches: [], findMatchIndex: -1, decimalPlaces: 2, }; const elements = {}; function init() { cacheElements(); renderGrid(); bindEvents(); loadFromUrlParams(); connectWebSocket(); connectChatWebSocket(); selectCell(0, 0); updateCellAddress(); renderCharts(); renderImages(); } 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"); elements.findReplaceModal = document.getElementById("findReplaceModal"); elements.conditionalFormatModal = document.getElementById( "conditionalFormatModal", ); elements.dataValidationModal = document.getElementById( "dataValidationModal", ); elements.printPreviewModal = document.getElementById("printPreviewModal"); elements.customNumberFormatModal = document.getElementById( "customNumberFormatModal", ); elements.insertImageModal = document.getElementById("insertImageModal"); } 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("numberFormat") ?.addEventListener("change", handleNumberFormatChange); document .getElementById("decreaseDecimalBtn") ?.addEventListener("click", decreaseDecimal); document .getElementById("increaseDecimalBtn") ?.addEventListener("click", increaseDecimal); 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("findReplaceBtn") ?.addEventListener("click", showFindReplaceModal); document .getElementById("closeFindReplaceModal") ?.addEventListener("click", () => hideModal("findReplaceModal")); document.getElementById("findNextBtn")?.addEventListener("click", findNext); document.getElementById("findPrevBtn")?.addEventListener("click", findPrev); document .getElementById("replaceBtn") ?.addEventListener("click", replaceOne); document .getElementById("replaceAllBtn") ?.addEventListener("click", replaceAll); document .getElementById("findInput") ?.addEventListener("input", performFind); document .getElementById("conditionalFormatBtn") ?.addEventListener("click", showConditionalFormatModal); document .getElementById("closeConditionalFormatModal") ?.addEventListener("click", () => hideModal("conditionalFormatModal")); document .getElementById("applyCfBtn") ?.addEventListener("click", applyConditionalFormat); document .getElementById("cancelCfBtn") ?.addEventListener("click", () => hideModal("conditionalFormatModal")); document .getElementById("cfRuleType") ?.addEventListener("change", handleCfRuleTypeChange); document .getElementById("cfBgColor") ?.addEventListener("input", updateCfPreview); document .getElementById("cfTextColor") ?.addEventListener("input", updateCfPreview); document .getElementById("cfBold") ?.addEventListener("change", updateCfPreview); document .getElementById("cfItalic") ?.addEventListener("change", updateCfPreview); document .getElementById("dataValidationBtn") ?.addEventListener("click", showDataValidationModal); document .getElementById("closeDataValidationModal") ?.addEventListener("click", () => hideModal("dataValidationModal")); document .getElementById("applyDvBtn") ?.addEventListener("click", applyDataValidation); document .getElementById("cancelDvBtn") ?.addEventListener("click", () => hideModal("dataValidationModal")); document .getElementById("clearDvBtn") ?.addEventListener("click", clearDataValidation); document .getElementById("dvType") ?.addEventListener("change", handleDvTypeChange); document .getElementById("dvOperator") ?.addEventListener("change", handleDvOperatorChange); document.querySelectorAll(".dv-tab").forEach((tab) => { tab.addEventListener("click", () => switchDvTab(tab.dataset.tab)); }); document .getElementById("printPreviewBtn") ?.addEventListener("click", showPrintPreview); document .getElementById("closePrintPreviewModal") ?.addEventListener("click", () => hideModal("printPreviewModal")); document.getElementById("printBtn")?.addEventListener("click", printSheet); document .getElementById("cancelPrintBtn") ?.addEventListener("click", () => hideModal("printPreviewModal")); document .getElementById("printOrientation") ?.addEventListener("change", updatePrintPreview); document .getElementById("printPaperSize") ?.addEventListener("change", updatePrintPreview); document .getElementById("printScale") ?.addEventListener("change", updatePrintPreview); document .getElementById("printGridlines") ?.addEventListener("change", updatePrintPreview); document .getElementById("printHeaders") ?.addEventListener("change", updatePrintPreview); document .getElementById("insertChartBtn") ?.addEventListener("click", () => showModal("chartModal")); document .getElementById("insertChartBtnConfirm") ?.addEventListener("click", insertChart); document .getElementById("cancelChartBtn") ?.addEventListener("click", () => hideModal("chartModal")); document .getElementById("insertImageBtn") ?.addEventListener("click", showInsertImageModal); document .getElementById("closeInsertImageModal") ?.addEventListener("click", () => hideModal("insertImageModal")); document .getElementById("insertImgBtn") ?.addEventListener("click", insertImage); document .getElementById("cancelImgBtn") ?.addEventListener("click", () => hideModal("insertImageModal")); document.querySelectorAll(".img-tab").forEach((tab) => { tab.addEventListener("click", () => switchImgTab(tab.dataset.tab)); }); document .getElementById("filterBtn") ?.addEventListener("click", toggleFilter); document .getElementById("sortAscBtn") ?.addEventListener("click", sortAscending); document .getElementById("sortDescBtn") ?.addEventListener("click", sortDescending); document .getElementById("closeCustomFormatModal") ?.addEventListener("click", () => hideModal("customNumberFormatModal")); document .getElementById("applyCnfBtn") ?.addEventListener("click", applyCustomNumberFormat); document .getElementById("cancelCnfBtn") ?.addEventListener("click", () => hideModal("customNumberFormatModal")); document.querySelectorAll(".cnf-format-item").forEach((item) => { item.addEventListener("click", () => selectCustomFormat(item.dataset.format), ); }); document .getElementById("cnfFormatCode") ?.addEventListener("input", updateCnfPreview); 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() { const { start, end } = state.selection; if (start.row === end.row && start.col === end.col) { addChatMessage("assistant", "Select multiple cells to merge."); return; } saveToHistory(); const ws = state.worksheets[state.activeWorksheet]; const firstKey = `${start.row},${start.col}`; let mergedValue = ""; for (let r = start.row; r <= end.row; r++) { for (let c = start.col; c <= end.col; c++) { const key = `${r},${c}`; const cellData = ws.data[key]; if (cellData?.value && !mergedValue) { mergedValue = cellData.value; } if (r !== start.row || c !== start.col) { delete ws.data[key]; } } } if (!ws.data[firstKey]) ws.data[firstKey] = {}; ws.data[firstKey].value = mergedValue; ws.data[firstKey].merged = { rowSpan: end.row - start.row + 1, colSpan: end.col - start.col + 1, }; renderAllCells(); state.isDirty = true; scheduleAutoSave(); addChatMessage("assistant", "Cells merged successfully!"); } 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) => `
${escapeHtml(ws.name)}
`, ) .join(""); elements.worksheetTabs.querySelectorAll(".sheet-tab").forEach((tab) => { tab.addEventListener("click", () => switchWorksheet(parseInt(tab.dataset.index)), ); }); } function zoomIn() { state.zoom = Math.min(200, state.zoom + 10); applyZoom(); } function zoomOut() { state.zoom = Math.max(50, state.zoom - 10); applyZoom(); } function applyZoom() { const scale = state.zoom / 100; elements.cells.style.transform = `scale(${scale})`; elements.cells.style.transformOrigin = "top left"; elements.zoomLevel.textContent = state.zoom + "%"; } function showModal(id) { document.getElementById(id)?.classList.remove("hidden"); } function hideModal(id) { document.getElementById(id)?.classList.add("hidden"); } function showShareModal() { const link = document.getElementById("shareLink"); if (link) link.value = window.location.href; showModal("shareModal"); } function copyShareLink() { const input = document.getElementById("shareLink"); if (input) { navigator.clipboard.writeText(input.value); } } function scheduleAutoSave() { if (state.autoSaveTimer) clearTimeout(state.autoSaveTimer); state.autoSaveTimer = setTimeout(() => { if (state.isDirty) saveSheet(); }, CONFIG.AUTOSAVE_DELAY); } async function saveSheet() { elements.saveStatus.textContent = "Saving..."; try { const response = await fetch("/api/sheet/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ id: state.sheetId, name: state.sheetName, worksheets: state.worksheets, }), }); if (response.ok) { const result = await response.json(); if (result.id) { state.sheetId = result.id; window.history.replaceState({}, "", `#id=${state.sheetId}`); } state.isDirty = false; elements.saveStatus.textContent = "Saved"; } else { elements.saveStatus.textContent = "Save failed"; } } catch (e) { elements.saveStatus.textContent = "Save failed"; } } async function loadFromUrlParams() { const hash = window.location.hash; if (!hash) return; const params = new URLSearchParams(hash.substring(1)); const sheetId = params.get("id"); if (sheetId) { try { const response = await fetch(`/api/sheet/${sheetId}`); if (response.ok) { const data = await response.json(); state.sheetId = sheetId; state.sheetName = data.name || "Untitled Spreadsheet"; state.worksheets = data.worksheets || [{ name: "Sheet1", data: {} }]; if (elements.sheetName) elements.sheetName.value = state.sheetName; renderWorksheetTabs(); renderAllCells(); } } catch (e) { console.error("Load failed:", e); } } } function handleBeforeUnload(e) { if (state.isDirty) { e.preventDefault(); e.returnValue = ""; } } function connectWebSocket() { if (!state.sheetId) return; const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const wsUrl = `${protocol}//${window.location.host}/api/sheet/ws/${state.sheetId}`; try { state.ws = new WebSocket(wsUrl); state.ws.onopen = () => { state.ws.send( JSON.stringify({ type: "join", sheetId: state.sheetId, userId: getUserId(), userName: getUserName(), }), ); }; state.ws.onmessage = (event) => { const msg = JSON.parse(event.data); handleWebSocketMessage(msg); }; state.ws.onclose = () => { setTimeout(connectWebSocket, CONFIG.WS_RECONNECT_DELAY); }; } catch (e) { console.error("WebSocket failed:", e); } } function handleWebSocketMessage(msg) { switch (msg.type) { case "cellChange": if (msg.userId !== getUserId()) { const ws = state.worksheets[state.activeWorksheet]; const key = `${msg.row},${msg.col}`; if (msg.value) { ws.data[key] = { value: msg.value }; } else { delete ws.data[key]; } renderCell(msg.row, msg.col); } break; case "cursor": updateRemoteCursor(msg); break; case "userJoined": addCollaborator(msg.user); break; case "userLeft": removeCollaborator(msg.userId); break; } } function broadcastChange(type, data) { if (state.ws?.readyState === WebSocket.OPEN) { state.ws.send( JSON.stringify({ type, sheetId: state.sheetId, userId: getUserId(), ...data, }), ); } } function updateRemoteCursor(msg) { let cursor = document.getElementById(`cursor-${msg.userId}`); if (!cursor) { cursor = document.createElement("div"); cursor.id = `cursor-${msg.userId}`; cursor.className = "cursor-indicator"; cursor.style.borderColor = msg.color || "#4285f4"; cursor.innerHTML = `
${escapeHtml(msg.userName)}
`; elements.cursorIndicators?.appendChild(cursor); } const cell = elements.cells.querySelector( `[data-row="${msg.row}"][data-col="${msg.col}"]`, ); if (cell) { const rect = cell.getBoundingClientRect(); const container = elements.cellsContainer.getBoundingClientRect(); cursor.style.left = rect.left - container.left + "px"; cursor.style.top = rect.top - container.top + "px"; cursor.style.width = rect.width + "px"; cursor.style.height = rect.height + "px"; } } function addCollaborator(user) { if (!state.collaborators.find((u) => u.id === user.id)) { state.collaborators.push(user); renderCollaborators(); } } function removeCollaborator(userId) { state.collaborators = state.collaborators.filter((u) => u.id !== userId); document.getElementById(`cursor-${userId}`)?.remove(); renderCollaborators(); } function renderCollaborators() { elements.collaborators.innerHTML = state.collaborators .slice(0, 4) .map( (u) => `
${u.name.charAt(0).toUpperCase()}
`, ) .join(""); } function getUserId() { let id = localStorage.getItem("gb-user-id"); if (!id) { id = "user-" + Math.random().toString(36).substr(2, 9); localStorage.setItem("gb-user-id", id); } return id; } function getUserName() { return localStorage.getItem("gb-user-name") || "Anonymous"; } function escapeHtml(str) { if (!str) return ""; return String(str) .replace(/&/g, "&") .replace(//g, ">"); } function toggleChatPanel() { state.chatPanelOpen = !state.chatPanelOpen; elements.chatPanel.classList.toggle("collapsed", !state.chatPanelOpen); } function handleChatSubmit(e) { e.preventDefault(); const message = elements.chatInput.value.trim(); if (!message) return; addChatMessage("user", message); elements.chatInput.value = ""; processAICommand(message); } function handleSuggestionClick(action) { const commands = { sum: "Sum column B", format: "Format selected cells as currency", chart: "Create a bar chart from selected data", sort: "Sort selected column A to Z", }; const message = commands[action] || action; addChatMessage("user", message); processAICommand(message); } function addChatMessage(role, content) { const div = document.createElement("div"); div.className = `chat-message ${role}`; div.innerHTML = `
${escapeHtml(content)}
`; elements.chatMessages.appendChild(div); elements.chatMessages.scrollTop = elements.chatMessages.scrollHeight; } async function processAICommand(command) { const lower = command.toLowerCase(); let response = ""; if (lower.includes("sum")) { const { start, end } = state.selection; const colLetter = getColName(start.col); const formula = `=SUM(${colLetter}${start.row + 1}:${colLetter}${end.row + 1})`; const resultRow = end.row + 1; if (resultRow < CONFIG.ROWS) { setCellValue(resultRow, start.col, formula); renderCell(resultRow, start.col); selectCell(resultRow, start.col); response = `Done! Added SUM formula in cell ${getColName(start.col)}${resultRow + 1}`; } else { response = "Cannot add sum - no row available below selection"; } } else if (lower.includes("currency") || lower.includes("$")) { formatCells("currency"); response = "Formatted selected cells as currency"; } else if (lower.includes("percent") || lower.includes("%")) { formatCells("percent"); response = "Formatted selected cells as percentage"; } else if (lower.includes("bold")) { formatCells("bold"); response = "Applied bold formatting to selected cells"; } else if (lower.includes("italic")) { formatCells("italic"); response = "Applied italic formatting to selected cells"; } else if (lower.includes("sort") && lower.includes("z")) { sortDescending(); response = "Sorted selection Z to A"; } else if (lower.includes("sort")) { sortAscending(); response = "Sorted selection A to Z"; } else if (lower.includes("chart")) { showModal("chartModal"); response = "Opening chart dialog. Select chart type and configure options."; } else if (lower.includes("clear")) { clearCells(); response = "Cleared selected cells"; } else if (lower.includes("average") || lower.includes("avg")) { const { start, end } = state.selection; const colLetter = getColName(start.col); const formula = `=AVERAGE(${colLetter}${start.row + 1}:${colLetter}${end.row + 1})`; const resultRow = end.row + 1; if (resultRow < CONFIG.ROWS) { setCellValue(resultRow, start.col, formula); renderCell(resultRow, start.col); selectCell(resultRow, start.col); response = `Done! Added AVERAGE formula in cell ${getColName(start.col)}${resultRow + 1}`; } } else { try { const res = await fetch("/api/sheet/ai", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ command, selection: state.selection, activeCell: state.activeCell, sheetId: state.sheetId, }), }); const data = await res.json(); response = data.response || "I processed your request"; } catch { response = "I can help you with:\n• Sum/Average a column\n• Format as currency or percent\n• Bold/Italic formatting\n• Sort data\n• Create charts\n• Clear cells"; } } addChatMessage("assistant", response); } function sortAscending() { sortSelection(true); } function sortDescending() { sortSelection(false); } function sortSelection(ascending) { saveToHistory(); const { start, end } = state.selection; const ws = state.worksheets[state.activeWorksheet]; const rows = []; 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 || a.data[0]?.formula || ""; const valB = b.data[0]?.value || b.data[0]?.formula || ""; const numA = parseFloat(valA); const numB = parseFloat(valB); if (!isNaN(numA) && !isNaN(numB)) { return ascending ? numA - numB : numB - numA; } return ascending ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(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(); } function connectChatWebSocket() { // Chat uses main WebSocket connection } function handleNumberFormatChange(e) { const format = e.target.value; if (format === "custom") { showModal("customNumberFormatModal"); return; } applyNumberFormat(format); } function applyNumberFormat(format) { 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: "" }; ws.data[key].format = format; const rawValue = ws.data[key].rawValue || ws.data[key].value; if (rawValue) { ws.data[key].rawValue = rawValue; ws.data[key].value = formatValue(rawValue, format); } renderCell(r, c); } } state.isDirty = true; scheduleAutoSave(); } function formatValue(value, format) { const num = parseFloat(value); if (isNaN(num) && format !== "text") return value; switch (format) { case "number": return num.toLocaleString("en-US", { minimumFractionDigits: state.decimalPlaces, maximumFractionDigits: state.decimalPlaces, }); case "currency": return num.toLocaleString("en-US", { style: "currency", currency: "USD", minimumFractionDigits: state.decimalPlaces, }); case "accounting": const formatted = Math.abs(num).toLocaleString("en-US", { style: "currency", currency: "USD", }); return num < 0 ? `(${formatted})` : formatted; case "percent": return (num * 100).toFixed(state.decimalPlaces) + "%"; case "scientific": return num.toExponential(state.decimalPlaces); case "date_short": const d1 = new Date(num); return isNaN(d1.getTime()) ? value : d1.toLocaleDateString("en-US"); case "date_long": const d2 = new Date(num); return isNaN(d2.getTime()) ? value : d2.toLocaleDateString("en-US", { year: "numeric", month: "long", day: "numeric", }); case "time": const d3 = new Date(num); return isNaN(d3.getTime()) ? value : d3.toLocaleTimeString("en-US", { hour: "numeric", minute: "2-digit", }); case "datetime": const d4 = new Date(num); return isNaN(d4.getTime()) ? value : d4.toLocaleString("en-US"); case "fraction": return toFraction(num); case "text": return String(value); default: return value; } } function toFraction(decimal) { const tolerance = 1e-6; let h1 = 1, h2 = 0, k1 = 0, k2 = 1; let b = decimal; do { const a = Math.floor(b); let aux = h1; h1 = a * h1 + h2; h2 = aux; aux = k1; k1 = a * k1 + k2; k2 = aux; b = 1 / (b - a); } while (Math.abs(decimal - h1 / k1) > decimal * tolerance); if (k1 === 1) return String(h1); const whole = Math.floor(h1 / k1); const remainder = h1 % k1; if (whole === 0) return `${remainder}/${k1}`; return `${whole} ${remainder}/${k1}`; } function decreaseDecimal() { if (state.decimalPlaces > 0) { state.decimalPlaces--; reapplyFormats(); } } function increaseDecimal() { if (state.decimalPlaces < 10) { state.decimalPlaces++; reapplyFormats(); } } function reapplyFormats() { 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}`; const cellData = ws.data[key]; if (cellData?.format && cellData?.rawValue) { cellData.value = formatValue(cellData.rawValue, cellData.format); renderCell(r, c); } } } } function showFindReplaceModal() { showModal("findReplaceModal"); document.getElementById("findInput")?.focus(); state.findMatches = []; state.findMatchIndex = -1; } function performFind() { const searchText = document.getElementById("findInput")?.value || ""; const matchCase = document.getElementById("findMatchCase")?.checked; const wholeCell = document.getElementById("findWholeCell")?.checked; const useRegex = document.getElementById("findRegex")?.checked; state.findMatches = []; state.findMatchIndex = -1; if (!searchText) { updateFindResults(); return; } const ws = state.worksheets[state.activeWorksheet]; let pattern; if (useRegex) { try { pattern = new RegExp(searchText, matchCase ? "" : "i"); } catch (e) { updateFindResults(); return; } } for (let r = 0; r < CONFIG.ROWS; r++) { for (let c = 0; c < CONFIG.COLS; c++) { const key = `${r},${c}`; const cellData = ws.data[key]; const cellValue = cellData?.value || ""; if (!cellValue) continue; let matches = false; const compareValue = matchCase ? cellValue : cellValue.toLowerCase(); const compareSearch = matchCase ? searchText : searchText.toLowerCase(); if (useRegex) { matches = pattern.test(cellValue); } else if (wholeCell) { matches = compareValue === compareSearch; } else { matches = compareValue.includes(compareSearch); } if (matches) { state.findMatches.push({ row: r, col: c }); } } } updateFindResults(); if (state.findMatches.length > 0) { state.findMatchIndex = 0; highlightFindMatch(); } } function updateFindResults() { const resultsEl = document.getElementById("findResults"); if (resultsEl) { const count = state.findMatches.length; resultsEl.querySelector("span").textContent = count === 0 ? "0 matches found" : `${state.findMatchIndex + 1} of ${count} matches`; } } function highlightFindMatch() { if (state.findMatches.length === 0) return; const match = state.findMatches[state.findMatchIndex]; selectCell(match.row, match.col); updateFindResults(); } function findNext() { if (state.findMatches.length === 0) return; state.findMatchIndex = (state.findMatchIndex + 1) % state.findMatches.length; highlightFindMatch(); } function findPrev() { if (state.findMatches.length === 0) return; state.findMatchIndex = (state.findMatchIndex - 1 + state.findMatches.length) % state.findMatches.length; highlightFindMatch(); } function replaceOne() { if (state.findMatches.length === 0 || state.findMatchIndex < 0) return; const replaceText = document.getElementById("replaceInput")?.value || ""; const match = state.findMatches[state.findMatchIndex]; const ws = state.worksheets[state.activeWorksheet]; const key = `${match.row},${match.col}`; saveToHistory(); const searchText = document.getElementById("findInput")?.value || ""; const matchCase = document.getElementById("findMatchCase")?.checked; const useRegex = document.getElementById("findRegex")?.checked; const cellValue = ws.data[key]?.value || ""; let newValue; if (useRegex) { const pattern = new RegExp(searchText, matchCase ? "g" : "gi"); newValue = cellValue.replace(pattern, replaceText); } else { const flags = matchCase ? "g" : "gi"; const escapedSearch = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); newValue = cellValue.replace( new RegExp(escapedSearch, flags), replaceText, ); } if (!ws.data[key]) ws.data[key] = {}; ws.data[key].value = newValue; renderCell(match.row, match.col); state.findMatches.splice(state.findMatchIndex, 1); if (state.findMatches.length > 0) { state.findMatchIndex = state.findMatchIndex % state.findMatches.length; highlightFindMatch(); } else { state.findMatchIndex = -1; updateFindResults(); } state.isDirty = true; scheduleAutoSave(); } function replaceAll() { if (state.findMatches.length === 0) return; const replaceText = document.getElementById("replaceInput")?.value || ""; const searchText = document.getElementById("findInput")?.value || ""; const matchCase = document.getElementById("findMatchCase")?.checked; const useRegex = document.getElementById("findRegex")?.checked; const ws = state.worksheets[state.activeWorksheet]; saveToHistory(); let count = 0; for (const match of state.findMatches) { const key = `${match.row},${match.col}`; const cellValue = ws.data[key]?.value || ""; let newValue; if (useRegex) { const pattern = new RegExp(searchText, matchCase ? "g" : "gi"); newValue = cellValue.replace(pattern, replaceText); } else { const flags = matchCase ? "g" : "gi"; const escapedSearch = searchText.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); newValue = cellValue.replace( new RegExp(escapedSearch, flags), replaceText, ); } if (!ws.data[key]) ws.data[key] = {}; ws.data[key].value = newValue; renderCell(match.row, match.col); count++; } state.findMatches = []; state.findMatchIndex = -1; updateFindResults(); state.isDirty = true; scheduleAutoSave(); addChatMessage("assistant", `Replaced ${count} occurrences.`); } function showConditionalFormatModal() { const { start, end } = state.selection; const range = `${getColName(start.col)}${start.row + 1}:${getColName(end.col)}${end.row + 1}`; const rangeInput = document.getElementById("cfRange"); if (rangeInput) rangeInput.value = range; showModal("conditionalFormatModal"); handleCfRuleTypeChange(); updateCfPreview(); } function handleCfRuleTypeChange() { const ruleType = document.getElementById("cfRuleType")?.value; const value2 = document.getElementById("cfValue2"); const valuesSection = document.getElementById("cfValuesSection"); if (value2) { if (ruleType === "between") { value2.classList.remove("hidden"); value2.placeholder = "and"; } else { value2.classList.add("hidden"); } } const noValueTypes = [ "duplicate", "unique", "blank", "not_blank", "above_average", "below_average", "color_scale", "data_bar", "icon_set", ]; if (valuesSection) { if (noValueTypes.includes(ruleType)) { valuesSection.style.display = "none"; } else { valuesSection.style.display = "flex"; } } } function updateCfPreview() { const bgColor = document.getElementById("cfBgColor")?.value || "#ffeb3b"; const textColor = document.getElementById("cfTextColor")?.value || "#000000"; const bold = document.getElementById("cfBold")?.checked; const italic = document.getElementById("cfItalic")?.checked; const previewCell = document.getElementById("cfPreviewCell"); if (previewCell) { previewCell.style.background = bgColor; previewCell.style.color = textColor; previewCell.style.fontWeight = bold ? "bold" : "normal"; previewCell.style.fontStyle = italic ? "italic" : "normal"; } } function applyConditionalFormat() { const rangeStr = document.getElementById("cfRange")?.value; if (!rangeStr) { alert("Please specify a range."); return; } const ruleType = document.getElementById("cfRuleType")?.value; const value1 = document.getElementById("cfValue1")?.value; const value2 = document.getElementById("cfValue2")?.value; const bgColor = document.getElementById("cfBgColor")?.value; const textColor = document.getElementById("cfTextColor")?.value; const bold = document.getElementById("cfBold")?.checked; const italic = document.getElementById("cfItalic")?.checked; const ws = state.worksheets[state.activeWorksheet]; if (!ws.conditionalFormats) ws.conditionalFormats = []; const rule = { id: `cf_${Date.now()}`, range: rangeStr, ruleType, value1, value2, style: { background: bgColor, color: textColor, fontWeight: bold ? "bold" : "normal", fontStyle: italic ? "italic" : "normal", }, }; ws.conditionalFormats.push(rule); applyConditionalFormatsToRange(rule); hideModal("conditionalFormatModal"); state.isDirty = true; scheduleAutoSave(); addChatMessage("assistant", "Conditional formatting applied!"); } function applyConditionalFormatsToRange(rule) { const ws = state.worksheets[state.activeWorksheet]; const rangeParts = rule.range.split(":"); if (rangeParts.length !== 2) return; const startRef = parseCellRef(rangeParts[0]); const endRef = parseCellRef(rangeParts[1]); if (!startRef || !endRef) return; for (let r = startRef.row; r <= endRef.row; r++) { for (let c = startRef.col; c <= endRef.col; c++) { const key = `${r},${c}`; const cellData = ws.data[key]; const cellValue = parseFloat(cellData?.value) || 0; let conditionMet = false; switch (rule.ruleType) { case "greater_than": conditionMet = cellValue > parseFloat(rule.value1); break; case "less_than": conditionMet = cellValue < parseFloat(rule.value1); break; case "equal_to": conditionMet = cellValue === parseFloat(rule.value1); break; case "between": conditionMet = cellValue >= parseFloat(rule.value1) && cellValue <= parseFloat(rule.value2); break; case "text_contains": conditionMet = (cellData?.value || "") .toLowerCase() .includes(rule.value1.toLowerCase()); break; case "blank": conditionMet = !cellData?.value; break; case "not_blank": conditionMet = !!cellData?.value; break; default: conditionMet = false; } if (conditionMet && cellData) { if (!cellData.style) cellData.style = {}; Object.assign(cellData.style, rule.style); renderCell(r, c); } } } } function showDataValidationModal() { const { start, end } = state.selection; const range = `${getColName(start.col)}${start.row + 1}:${getColName(end.col)}${end.row + 1}`; const rangeInput = document.getElementById("dvRange"); if (rangeInput) rangeInput.value = range; showModal("dataValidationModal"); handleDvTypeChange(); } function switchDvTab(tabName) { document.querySelectorAll(".dv-tab").forEach((tab) => { tab.classList.toggle("active", tab.dataset.tab === tabName); }); document.querySelectorAll(".dv-tab-content").forEach((content) => { const contentId = content.id .replace("dv", "") .replace("Tab", "") .toLowerCase(); content.classList.toggle("active", contentId === tabName); }); } function handleDvTypeChange() { const dvType = document.getElementById("dvType")?.value; const criteriaSection = document.getElementById("dvCriteriaSection"); const valuesSection = document.getElementById("dvValuesSection"); const listSection = document.getElementById("dvListSection"); const value2Row = document.getElementById("dvValue2Row"); const value1Label = document.getElementById("dvValue1Label"); if (criteriaSection) { criteriaSection.style.display = dvType === "any" || dvType === "list" || dvType === "custom" ? "none" : "block"; } if (valuesSection) { valuesSection.style.display = dvType === "any" || dvType === "list" ? "none" : "block"; } if (listSection) { listSection.classList.toggle("hidden", dvType !== "list"); } if (value1Label) { value1Label.textContent = dvType === "custom" ? "Formula:" : "Minimum:"; } } function handleDvOperatorChange() { const operator = document.getElementById("dvOperator")?.value; const value2Row = document.getElementById("dvValue2Row"); const value1Label = document.getElementById("dvValue1Label"); if (value2Row) { value2Row.style.display = operator === "between" || operator === "not_between" ? "block" : "none"; } if (value1Label) { if (operator === "between" || operator === "not_between") { value1Label.textContent = "Minimum:"; } else { value1Label.textContent = "Value:"; } } } function applyDataValidation() { const rangeStr = document.getElementById("dvRange")?.value; if (!rangeStr) { alert("Please specify a range."); return; } const dvType = document.getElementById("dvType")?.value; const operator = document.getElementById("dvOperator")?.value; const value1 = document.getElementById("dvValue1")?.value; const value2 = document.getElementById("dvValue2")?.value; const listSource = document.getElementById("dvListSource")?.value; const showInput = document.getElementById("dvShowInput")?.checked; const inputTitle = document.getElementById("dvInputTitle")?.value; const inputMessage = document.getElementById("dvInputMessage")?.value; const showError = document.getElementById("dvShowError")?.checked; const errorStyle = document.getElementById("dvErrorStyle")?.value; const errorTitle = document.getElementById("dvErrorTitle")?.value; const errorMessage = document.getElementById("dvErrorMessage")?.value; const ws = state.worksheets[state.activeWorksheet]; if (!ws.validations) ws.validations = {}; const validation = { type: dvType, operator, value1, value2, listValues: listSource ? listSource.split(",").map((s) => s.trim()) : [], showInput, inputTitle, inputMessage, showError, errorStyle, errorTitle, errorMessage, }; const rangeParts = rangeStr.split(":"); const startRef = parseCellRef(rangeParts[0]); const endRef = rangeParts.length > 1 ? parseCellRef(rangeParts[1]) : startRef; if (startRef && endRef) { for (let r = startRef.row; r <= endRef.row; r++) { for (let c = startRef.col; c <= endRef.col; c++) { ws.validations[`${r},${c}`] = validation; } } } hideModal("dataValidationModal"); state.isDirty = true; scheduleAutoSave(); addChatMessage("assistant", "Data validation applied!"); } function clearDataValidation() { const rangeStr = document.getElementById("dvRange")?.value; if (!rangeStr) return; const ws = state.worksheets[state.activeWorksheet]; if (!ws.validations) return; const rangeParts = rangeStr.split(":"); const startRef = parseCellRef(rangeParts[0]); const endRef = rangeParts.length > 1 ? parseCellRef(rangeParts[1]) : startRef; if (startRef && endRef) { for (let r = startRef.row; r <= endRef.row; r++) { for (let c = startRef.col; c <= endRef.col; c++) { delete ws.validations[`${r},${c}`]; } } } hideModal("dataValidationModal"); state.isDirty = true; scheduleAutoSave(); } function showPrintPreview() { showModal("printPreviewModal"); updatePrintPreview(); } function updatePrintPreview() { const orientation = document.getElementById("printOrientation")?.value || "portrait"; const showGridlines = document.getElementById("printGridlines")?.checked; const showHeaders = document.getElementById("printHeaders")?.checked; const printPage = document.getElementById("printPage"); const printContent = document.getElementById("printContent"); if (printPage) { printPage.className = `print-page ${orientation}`; } if (!printContent) return; const ws = state.worksheets[state.activeWorksheet]; let html = ""; if (showHeaders) { html += ""; for (let c = 0; c < CONFIG.COLS; c++) { html += ``; } html += ""; } html += ""; let hasData = false; let maxRow = 0; let maxCol = 0; for (const key in ws.data) { if (ws.data[key]?.value) { hasData = true; const [r, c] = key.split(",").map(Number); maxRow = Math.max(maxRow, r); maxCol = Math.max(maxCol, c); } } if (!hasData) { maxRow = 10; maxCol = 5; } for (let r = 0; r <= maxRow; r++) { html += ""; if (showHeaders) { html += ``; } for (let c = 0; c <= maxCol; c++) { const key = `${r},${c}`; const cellData = ws.data[key]; const value = cellData?.value || ""; const style = cellData?.style || {}; let styleStr = ""; if (style.fontWeight) styleStr += `font-weight:${style.fontWeight};`; if (style.fontStyle) styleStr += `font-style:${style.fontStyle};`; if (style.textAlign) styleStr += `text-align:${style.textAlign};`; if (style.color) styleStr += `color:${style.color};`; if (style.background) styleStr += `background:${style.background};`; const borderStyle = showGridlines ? "" : "border:none;"; html += ``; } html += ""; } html += "
${getColName(c)}
${r + 1}${escapeHtml(value)}
"; printContent.innerHTML = html; } function printSheet() { const printContent = document.getElementById("printContent")?.innerHTML; if (!printContent) return; const orientation = document.getElementById("printOrientation")?.value || "portrait"; const printWindow = window.open("", "_blank"); printWindow.document.write(` ${state.sheetName} ${printContent} `); printWindow.document.close(); printWindow.focus(); setTimeout(() => { printWindow.print(); printWindow.close(); }, 250); hideModal("printPreviewModal"); } function insertChart() { const chartType = document.querySelector(".chart-type-btn.active")?.dataset.type || "bar"; const dataRange = document.getElementById("chartDataRange")?.value; const chartTitle = document.getElementById("chartTitle")?.value || "Chart"; if (!dataRange) { alert("Please specify a data range."); return; } const ws = state.worksheets[state.activeWorksheet]; if (!ws.charts) ws.charts = []; const chart = { id: `chart_${Date.now()}`, type: chartType, title: chartTitle, dataRange, position: { row: state.activeCell.row, col: state.activeCell.col, width: 400, height: 300, }, }; ws.charts.push(chart); hideModal("chartModal"); state.isDirty = true; scheduleAutoSave(); addChatMessage( "assistant", `${chartType.charAt(0).toUpperCase() + chartType.slice(1)} chart created!`, ); } function showInsertImageModal() { showModal("insertImageModal"); } function switchImgTab(tabName) { document.querySelectorAll(".img-tab").forEach((tab) => { tab.classList.toggle("active", tab.dataset.tab === tabName); }); document.querySelectorAll(".img-tab-content").forEach((content) => { const contentId = content.id .replace("img", "") .replace("Tab", "") .toLowerCase(); content.classList.toggle("active", contentId === tabName); }); } function insertImage() { const urlTab = document.getElementById("imgUrlTab"); const isUrlTab = urlTab?.classList.contains("active"); let imageUrl; if (isUrlTab) { imageUrl = document.getElementById("imgUrl")?.value; } else { const fileInput = document.getElementById("imgFile"); if (fileInput?.files?.[0]) { addChatMessage( "assistant", "Image upload coming soon! Please use a URL for now.", ); return; } } if (!imageUrl) { alert("Please enter an image URL."); return; } const ws = state.worksheets[state.activeWorksheet]; if (!ws.images) ws.images = []; const image = { id: `img_${Date.now()}`, url: imageUrl, position: { row: state.activeCell.row, col: state.activeCell.col, width: 200, height: 150, }, }; ws.images.push(image); hideModal("insertImageModal"); state.isDirty = true; scheduleAutoSave(); addChatMessage("assistant", "Image inserted!"); } function toggleFilter() { const ws = state.worksheets[state.activeWorksheet]; ws.filterEnabled = !ws.filterEnabled; addChatMessage( "assistant", ws.filterEnabled ? "Filter enabled. Click column headers to filter." : "Filter disabled.", ); } function selectCustomFormat(formatCode) { document.querySelectorAll(".cnf-format-item").forEach((item) => { item.classList.toggle("selected", item.dataset.format === formatCode); }); const formatInput = document.getElementById("cnfFormatCode"); if (formatInput) { formatInput.value = formatCode; } updateCnfPreview(); } function updateCnfPreview() { const formatCode = document.getElementById("cnfFormatCode")?.value || "#,##0.00"; const previewEl = document.getElementById("cnfPreview"); if (!previewEl) return; const sampleValue = 1234.5678; let formatted; if (formatCode.includes("$")) { formatted = sampleValue.toLocaleString("en-US", { style: "currency", currency: "USD", }); } else if (formatCode.includes("%")) { formatted = (sampleValue * 100).toFixed(2) + "%"; } else if (formatCode.includes("E")) { formatted = sampleValue.toExponential(2); } else if (formatCode.includes("MM") || formatCode.includes("DD")) { formatted = new Date().toLocaleDateString(); } else if (formatCode.includes("HH")) { formatted = new Date().toLocaleTimeString(); } else { const decimals = (formatCode.match(/0+$/)?.[0] || "").length; formatted = sampleValue.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals, }); } previewEl.textContent = formatted; } function applyCustomNumberFormat() { const formatCode = document.getElementById("cnfFormatCode")?.value; if (!formatCode) return; 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: "" }; ws.data[key].customFormat = formatCode; renderCell(r, c); } } hideModal("customNumberFormatModal"); state.isDirty = true; scheduleAutoSave(); } function renderCharts() { const chartsContainer = document.getElementById("chartsContainer"); if (!chartsContainer) return; const ws = state.worksheets[state.activeWorksheet]; if (!ws.charts || ws.charts.length === 0) { chartsContainer.innerHTML = ""; return; } chartsContainer.innerHTML = ws.charts .map((chart) => renderChartHTML(chart)) .join(""); chartsContainer.querySelectorAll(".chart-wrapper").forEach((wrapper) => { const chartId = wrapper.dataset.chartId; wrapper.addEventListener("click", () => selectChart(chartId)); wrapper.querySelector(".chart-delete")?.addEventListener("click", (e) => { e.stopPropagation(); deleteChart(chartId); }); wrapper .querySelector(".chart-header") ?.addEventListener("mousedown", (e) => { startDragChart(e, chartId); }); }); } function renderChartHTML(chart) { const { id, type, title, position, dataRange } = chart; const left = position?.col ? position.col * CONFIG.COL_WIDTH : 100; const top = position?.row ? position.row * CONFIG.ROW_HEIGHT : 100; const width = position?.width || 400; const height = position?.height || 300; const data = getChartData(dataRange); let chartContent = ""; switch (type) { case "bar": chartContent = renderBarChart(data, height - 80); break; case "line": chartContent = renderLineChart(data, width - 32, height - 80); break; case "pie": chartContent = renderPieChart(data, Math.min(width, height) - 100); break; default: chartContent = renderBarChart(data, height - 80); } return `

${escapeHtml(title || "Chart")}

${chartContent}
${renderChartLegend(data)}
`; } function getChartData(dataRange) { if (!dataRange) return { labels: [], values: [] }; const ws = state.worksheets[state.activeWorksheet]; const rangeParts = dataRange.split(":"); if (rangeParts.length !== 2) return { labels: [], values: [] }; const startRef = parseCellRef(rangeParts[0]); const endRef = parseCellRef(rangeParts[1]); if (!startRef || !endRef) return { labels: [], values: [] }; const labels = []; const values = []; if (startRef.col === endRef.col) { for (let r = startRef.row; r <= endRef.row; r++) { const key = `${r},${startRef.col}`; const cellData = ws.data[key]; const val = parseFloat(cellData?.value) || 0; values.push(val); labels.push(`Row ${r + 1}`); } } else { for (let c = startRef.col; c <= endRef.col; c++) { const key = `${startRef.row},${c}`; const cellData = ws.data[key]; const val = parseFloat(cellData?.value) || 0; values.push(val); labels.push(getColName(c)); } } return { labels, values }; } function renderBarChart(data, maxHeight) { if (!data.values.length) return '
No data
'; const maxVal = Math.max(...data.values, 1); const bars = data.values .map((val, i) => { const height = (val / maxVal) * maxHeight; return `
`; }) .join(""); return `
${bars}
`; } function renderLineChart(data, width, height) { if (!data.values.length) return '
No data
'; const maxVal = Math.max(...data.values, 1); const padding = 20; const chartWidth = width - padding * 2; const chartHeight = height - padding * 2; const points = data.values.map((val, i) => { const x = padding + (i / (data.values.length - 1 || 1)) * chartWidth; const y = padding + chartHeight - (val / maxVal) * chartHeight; return `${x},${y}`; }); const circles = data.values .map((val, i) => { const x = padding + (i / (data.values.length - 1 || 1)) * chartWidth; const y = padding + chartHeight - (val / maxVal) * chartHeight; return ``; }) .join(""); return ` ${circles} `; } function renderPieChart(data, size) { if (!data.values.length) return '
No data
'; const total = data.values.reduce((a, b) => a + b, 0) || 1; const colors = [ "#4285f4", "#34a853", "#fbbc04", "#ea4335", "#9c27b0", "#00bcd4", "#ff5722", ]; const cx = size / 2; const cy = size / 2; const r = size / 2 - 10; let startAngle = 0; const slices = data.values .map((val, i) => { const angle = (val / total) * 360; const endAngle = startAngle + angle; const largeArc = angle > 180 ? 1 : 0; const x1 = cx + r * Math.cos((startAngle * Math.PI) / 180); const y1 = cy + r * Math.sin((startAngle * Math.PI) / 180); const x2 = cx + r * Math.cos((endAngle * Math.PI) / 180); const y2 = cy + r * Math.sin((endAngle * Math.PI) / 180); const path = `M ${cx} ${cy} L ${x1} ${y1} A ${r} ${r} 0 ${largeArc} 1 ${x2} ${y2} Z`; startAngle = endAngle; return ``; }) .join(""); return `
${slices}
`; } function renderChartLegend(data) { const colors = [ "#4285f4", "#34a853", "#fbbc04", "#ea4335", "#9c27b0", "#00bcd4", "#ff5722", ]; const items = data.labels .map( (label, i) => `
${escapeHtml(label)}
`, ) .join(""); return `
${items}
`; } function selectChart(chartId) { document.querySelectorAll(".chart-wrapper").forEach((el) => { el.classList.toggle("selected", el.dataset.chartId === chartId); }); } function deleteChart(chartId) { const ws = state.worksheets[state.activeWorksheet]; if (!ws.charts) return; ws.charts = ws.charts.filter((c) => c.id !== chartId); renderCharts(); state.isDirty = true; scheduleAutoSave(); } function startDragChart(e, chartId) { const wrapper = document.querySelector(`[data-chart-id="${chartId}"]`); if (!wrapper) return; const startX = e.clientX; const startY = e.clientY; const startLeft = parseInt(wrapper.style.left) || 0; const startTop = parseInt(wrapper.style.top) || 0; const onMouseMove = (moveEvent) => { const dx = moveEvent.clientX - startX; const dy = moveEvent.clientY - startY; wrapper.style.left = `${startLeft + dx}px`; wrapper.style.top = `${startTop + dy}px`; }; const onMouseUp = () => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); const ws = state.worksheets[state.activeWorksheet]; const chart = ws.charts?.find((c) => c.id === chartId); if (chart) { chart.position.col = Math.round( parseInt(wrapper.style.left) / CONFIG.COL_WIDTH, ); chart.position.row = Math.round( parseInt(wrapper.style.top) / CONFIG.ROW_HEIGHT, ); state.isDirty = true; scheduleAutoSave(); } }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); } function renderImages() { const imagesContainer = document.getElementById("imagesContainer"); if (!imagesContainer) return; const ws = state.worksheets[state.activeWorksheet]; if (!ws.images || ws.images.length === 0) { imagesContainer.innerHTML = ""; return; } imagesContainer.innerHTML = ws.images .map((img) => { const left = img.position?.col ? img.position.col * CONFIG.COL_WIDTH : 100; const top = img.position?.row ? img.position.row * CONFIG.ROW_HEIGHT : 100; const width = img.position?.width || 200; const height = img.position?.height || 150; return `
Embedded image
`; }) .join(""); imagesContainer.querySelectorAll(".image-wrapper").forEach((wrapper) => { const imageId = wrapper.dataset.imageId; wrapper.addEventListener("click", () => selectImage(imageId)); wrapper.addEventListener("mousedown", (e) => { if (e.target.classList.contains("image-resize-handle")) { startResizeImage(e, imageId); } else { startDragImage(e, imageId); } }); }); } function selectImage(imageId) { document.querySelectorAll(".image-wrapper").forEach((el) => { el.classList.toggle("selected", el.dataset.imageId === imageId); }); } function startDragImage(e, imageId) { const wrapper = document.querySelector(`[data-image-id="${imageId}"]`); if (!wrapper) return; const startX = e.clientX; const startY = e.clientY; const startLeft = parseInt(wrapper.style.left) || 0; const startTop = parseInt(wrapper.style.top) || 0; const onMouseMove = (moveEvent) => { const dx = moveEvent.clientX - startX; const dy = moveEvent.clientY - startY; wrapper.style.left = `${startLeft + dx}px`; wrapper.style.top = `${startTop + dy}px`; }; const onMouseUp = () => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); const ws = state.worksheets[state.activeWorksheet]; const img = ws.images?.find((i) => i.id === imageId); if (img) { img.position.col = Math.round( parseInt(wrapper.style.left) / CONFIG.COL_WIDTH, ); img.position.row = Math.round( parseInt(wrapper.style.top) / CONFIG.ROW_HEIGHT, ); state.isDirty = true; scheduleAutoSave(); } }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); e.preventDefault(); } function startResizeImage(e, imageId) { const wrapper = document.querySelector(`[data-image-id="${imageId}"]`); if (!wrapper) return; const startX = e.clientX; const startY = e.clientY; const startWidth = parseInt(wrapper.style.width) || 200; const startHeight = parseInt(wrapper.style.height) || 150; const aspectRatio = startWidth / startHeight; const onMouseMove = (moveEvent) => { const dx = moveEvent.clientX - startX; const newWidth = Math.max(50, startWidth + dx); const newHeight = newWidth / aspectRatio; wrapper.style.width = `${newWidth}px`; wrapper.style.height = `${newHeight}px`; }; const onMouseUp = () => { document.removeEventListener("mousemove", onMouseMove); document.removeEventListener("mouseup", onMouseUp); const ws = state.worksheets[state.activeWorksheet]; const img = ws.images?.find((i) => i.id === imageId); if (img) { img.position.width = parseInt(wrapper.style.width); img.position.height = parseInt(wrapper.style.height); state.isDirty = true; scheduleAutoSave(); } }; document.addEventListener("mousemove", onMouseMove); document.addEventListener("mouseup", onMouseUp); e.preventDefault(); e.stopPropagation(); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", init); } else { init(); } })();