2026-01-06 22:57:00 -03:00
/ * = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
2026-01-11 09:56:44 -03:00
GB SHEET - Modern Spreadsheet with AI Chat
2026-01-06 22:57:00 -03:00
=== === === === === === === === === === === === === === === === === === === === === === === === === == * /
( function ( ) {
"use strict" ;
const CONFIG = {
COLS : 26 ,
ROWS : 100 ,
COL _WIDTH : 100 ,
ROW _HEIGHT : 24 ,
MAX _HISTORY : 50 ,
AUTOSAVE _DELAY : 3000 ,
2026-01-11 09:56:44 -03:00
WS _RECONNECT _DELAY : 3000 ,
2026-01-06 22:57:00 -03:00
} ;
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 ,
2026-01-11 09:56:44 -03:00
zoom : 100 ,
2026-01-06 22:57:00 -03:00
collaborators : [ ] ,
ws : null ,
isEditing : false ,
isSelecting : false ,
isDirty : false ,
autoSaveTimer : null ,
2026-01-11 09:56:44 -03:00
chatPanelOpen : true ,
2026-01-06 22:57:00 -03:00
} ;
2026-01-11 09:56:44 -03:00
const elements = { } ;
2026-01-06 22:57:00 -03:00
function init ( ) {
cacheElements ( ) ;
renderGrid ( ) ;
bindEvents ( ) ;
loadFromUrlParams ( ) ;
connectWebSocket ( ) ;
2026-01-11 09:56:44 -03:00
connectChatWebSocket ( ) ;
selectCell ( 0 , 0 ) ;
2026-01-06 22:57:00 -03:00
updateCellAddress ( ) ;
}
function cacheElements ( ) {
2026-01-11 09:56:44 -03:00
elements . app = document . getElementById ( "sheet-app" ) ;
elements . sheetName = document . getElementById ( "sheetName" ) ;
elements . columnHeaders = document . getElementById ( "columnHeaders" ) ;
elements . rowHeaders = document . getElementById ( "rowHeaders" ) ;
2026-01-06 22:57:00 -03:00
elements . cells = document . getElementById ( "cells" ) ;
2026-01-11 09:56:44 -03:00
elements . cellsContainer = document . getElementById ( "cellsContainer" ) ;
elements . formulaInput = document . getElementById ( "formulaInput" ) ;
elements . cellAddress = document . getElementById ( "cellAddress" ) ;
elements . worksheetTabs = document . getElementById ( "worksheetTabs" ) ;
2026-01-06 22:57:00 -03:00
elements . collaborators = document . getElementById ( "collaborators" ) ;
2026-01-11 09:56:44 -03:00
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" ) ;
2026-01-06 22:57:00 -03:00
}
function renderGrid ( ) {
elements . columnHeaders . innerHTML = "" ;
2026-01-11 09:56:44 -03:00
for ( let col = 0 ; col < CONFIG . COLS ; col ++ ) {
2026-01-06 22:57:00 -03:00
const header = document . createElement ( "div" ) ;
header . className = "column-header" ;
2026-01-11 09:56:44 -03:00
header . textContent = getColName ( col ) ;
header . dataset . col = col ;
2026-01-06 22:57:00 -03:00
elements . columnHeaders . appendChild ( header ) ;
}
elements . rowHeaders . innerHTML = "" ;
2026-01-11 09:56:44 -03:00
for ( let row = 0 ; row < CONFIG . ROWS ; row ++ ) {
2026-01-06 22:57:00 -03:00
const header = document . createElement ( "div" ) ;
header . className = "row-header" ;
2026-01-11 09:56:44 -03:00
header . textContent = row + 1 ;
header . dataset . row = row ;
2026-01-06 22:57:00 -03:00
elements . rowHeaders . appendChild ( header ) ;
}
elements . cells . innerHTML = "" ;
elements . cells . style . gridTemplateColumns = ` repeat( ${ CONFIG . COLS } , ${ CONFIG . COL _WIDTH } px) ` ;
2026-01-11 09:56:44 -03:00
for ( let row = 0 ; row < CONFIG . ROWS ; row ++ ) {
for ( let col = 0 ; col < CONFIG . COLS ; col ++ ) {
2026-01-06 22:57:00 -03:00
const cell = document . createElement ( "div" ) ;
cell . className = "cell" ;
2026-01-11 09:56:44 -03:00
cell . dataset . row = row ;
cell . dataset . col = col ;
2026-01-06 22:57:00 -03:00
elements . cells . appendChild ( cell ) ;
}
}
2026-01-11 09:56:44 -03:00
renderAllCells ( ) ;
2026-01-06 22:57:00 -03:00
}
function renderAllCells ( ) {
const ws = state . worksheets [ state . activeWorksheet ] ;
if ( ! ws ) return ;
2026-01-11 09:56:44 -03:00
const cells = elements . cells . querySelectorAll ( ".cell" ) ;
cells . forEach ( ( cell ) => {
const row = parseInt ( cell . dataset . row ) ;
const col = parseInt ( cell . dataset . col ) ;
renderCell ( row , col ) ;
} ) ;
2026-01-06 22:57:00 -03:00
}
function renderCell ( row , col ) {
2026-01-11 09:56:44 -03:00
const cell = elements . cells . querySelector (
` [data-row=" ${ row } "][data-col=" ${ col } "] ` ,
) ;
2026-01-06 22:57:00 -03:00
if ( ! cell ) return ;
const data = getCellData ( row , col ) ;
2026-01-11 09:56:44 -03:00
let displayValue = "" ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
if ( data ) {
if ( data . formula ) {
displayValue = evaluateFormula ( data . formula , row , col ) ;
} else if ( data . value !== undefined ) {
displayValue = data . value ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
applyFormatToCell ( cell , data . style ) ;
2026-01-06 22:57:00 -03:00
} else {
cell . style . cssText = "" ;
}
2026-01-11 09:56:44 -03:00
cell . textContent = displayValue ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
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 ;
}
2026-01-06 22:57:00 -03:00
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 ( ) {
2026-01-11 09:56:44 -03:00
elements . cells . addEventListener ( "mousedown" , handleCellMouseDown ) ;
elements . cells . addEventListener ( "dblclick" , handleCellDoubleClick ) ;
2026-01-06 22:57:00 -03:00
document . addEventListener ( "mousemove" , handleMouseMove ) ;
document . addEventListener ( "mouseup" , handleMouseUp ) ;
document . addEventListener ( "keydown" , handleKeyDown ) ;
2026-01-11 09:56:44 -03:00
document . addEventListener ( "click" , handleDocumentClick ) ;
document . addEventListener ( "contextmenu" , handleContextMenu ) ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
elements . columnHeaders . addEventListener ( "click" , handleColumnHeaderClick ) ;
elements . rowHeaders . addEventListener ( "click" , handleRowHeaderClick ) ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
elements . formulaInput . addEventListener ( "keydown" , handleFormulaKey ) ;
elements . formulaInput . addEventListener ( "input" , updateFormulaPreview ) ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
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 ( ) ;
} ) ;
2026-01-06 22:57:00 -03:00
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 ) ;
2026-01-11 09:56:44 -03:00
if ( state . isEditing ) {
finishEditing ( ) ;
}
2026-01-06 22:57:00 -03:00
if ( e . shiftKey ) {
extendSelection ( row , col ) ;
} else {
selectCell ( row , col ) ;
2026-01-11 09:56:44 -03:00
state . isSelecting = true ;
2026-01-06 22:57:00 -03:00
}
}
function handleMouseMove ( e ) {
2026-01-11 09:56:44 -03:00
if ( ! state . isSelecting ) return ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
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 ) ;
}
2026-01-06 22:57:00 -03:00
}
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 } ,
} ;
2026-01-11 09:56:44 -03:00
const cell = elements . cells . querySelector (
` [data-row=" ${ row } "][data-col=" ${ col } "] ` ,
) ;
2026-01-06 22:57:00 -03:00
if ( cell ) {
cell . classList . add ( "selected" ) ;
2026-01-11 09:56:44 -03:00
cell . scrollIntoView ( { block : "nearest" , inline : "nearest" } ) ;
2026-01-06 22:57:00 -03:00
}
updateCellAddress ( ) ;
updateFormulaBar ( ) ;
updateSelectionInfo ( ) ;
}
function extendSelection ( row , col ) {
2026-01-11 09:56:44 -03:00
clearSelection ( ) ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
const start = state . activeCell ;
2026-01-06 22:57:00 -03:00
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 ++
) {
2026-01-11 09:56:44 -03:00
const cell = elements . cells . querySelector (
` [data-row=" ${ r } "][data-col=" ${ c } "] ` ,
) ;
2026-01-06 22:57:00 -03:00
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 ( ) {
2026-01-11 09:56:44 -03:00
elements . cells
2026-01-06 22:57:00 -03:00
. 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 ) ;
2026-01-11 09:56:44 -03:00
clearSelection ( ) ;
state . activeCell = { row : 0 , col } ;
2026-01-06 22:57:00 -03:00
state . selection = {
start : { row : 0 , col } ,
end : { row : CONFIG . ROWS - 1 , col } ,
} ;
2026-01-11 09:56:44 -03:00
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" ) ;
2026-01-06 22:57:00 -03:00
}
header . classList . add ( "selected" ) ;
2026-01-11 09:56:44 -03:00
updateSelectionInfo ( ) ;
2026-01-06 22:57:00 -03:00
}
function handleRowHeaderClick ( e ) {
const header = e . target . closest ( ".row-header" ) ;
if ( ! header ) return ;
const row = parseInt ( header . dataset . row ) ;
2026-01-11 09:56:44 -03:00
clearSelection ( ) ;
state . activeCell = { row , col : 0 } ;
2026-01-06 22:57:00 -03:00
state . selection = {
start : { row , col : 0 } ,
end : { row , col : CONFIG . COLS - 1 } ,
} ;
2026-01-11 09:56:44 -03:00
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" ) ;
2026-01-06 22:57:00 -03:00
}
header . classList . add ( "selected" ) ;
2026-01-11 09:56:44 -03:00
updateSelectionInfo ( ) ;
2026-01-06 22:57:00 -03:00
}
function startEditing ( row , col ) {
2026-01-11 09:56:44 -03:00
const cell = elements . cells . querySelector (
` [data-row=" ${ row } "][data-col=" ${ col } "] ` ,
) ;
2026-01-06 22:57:00 -03:00
if ( ! cell ) return ;
state . isEditing = true ;
const data = getCellData ( row , col ) ;
const input = document . createElement ( "input" ) ;
input . type = "text" ;
input . className = "cell-input" ;
2026-01-11 09:56:44 -03:00
input . value = data ? . formula || data ? . value || "" ;
cell . textContent = "" ;
cell . classList . add ( "editing" ) ;
cell . appendChild ( input ) ;
input . focus ( ) ;
input . select ( ) ;
2026-01-06 22:57:00 -03:00
input . addEventListener ( "keydown" , ( e ) => {
if ( e . key === "Enter" ) {
2026-01-11 09:56:44 -03:00
finishEditing ( true ) ;
2026-01-06 22:57:00 -03:00
navigateCell ( 1 , 0 ) ;
} else if ( e . key === "Tab" ) {
e . preventDefault ( ) ;
2026-01-11 09:56:44 -03:00
finishEditing ( true ) ;
2026-01-06 22:57:00 -03:00
navigateCell ( 0 , e . shiftKey ? - 1 : 1 ) ;
} else if ( e . key === "Escape" ) {
2026-01-11 09:56:44 -03:00
cancelEditing ( ) ;
2026-01-06 22:57:00 -03:00
}
} ) ;
2026-01-11 09:56:44 -03:00
input . addEventListener ( "blur" , ( ) => {
if ( state . isEditing ) finishEditing ( true ) ;
} ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function finishEditing ( save = true ) {
if ( ! state . isEditing ) return ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
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 ( ) ;
2026-01-06 22:57:00 -03:00
setCellValue ( row , col , value ) ;
}
state . isEditing = false ;
2026-01-11 09:56:44 -03:00
cell ? . classList . remove ( "editing" ) ;
2026-01-06 22:57:00 -03:00
renderCell ( row , col ) ;
2026-01-11 09:56:44 -03:00
updateFormulaBar ( ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function cancelEditing ( ) {
2026-01-06 22:57:00 -03:00
state . isEditing = false ;
2026-01-11 09:56:44 -03:00
const { row , col } = state . activeCell ;
const cell = elements . cells . querySelector (
` [data-row=" ${ row } "][data-col=" ${ col } "] ` ,
) ;
cell ? . classList . remove ( "editing" ) ;
2026-01-06 22:57:00 -03:00
renderCell ( row , col ) ;
}
function setCellValue ( row , col , value ) {
const ws = state . worksheets [ state . activeWorksheet ] ;
const key = ` ${ row } , ${ col } ` ;
2026-01-11 09:56:44 -03:00
saveToHistory ( ) ;
if ( ! value ) {
2026-01-06 22:57:00 -03:00
delete ws . data [ key ] ;
2026-01-11 09:56:44 -03:00
} else if ( value . startsWith ( "=" ) ) {
ws . data [ key ] = { formula : value } ;
} else {
ws . data [ key ] = { value } ;
2026-01-06 22:57:00 -03:00
}
state . isDirty = true ;
scheduleAutoSave ( ) ;
2026-01-11 09:56:44 -03:00
broadcastChange ( "cell" , { row , col , value } ) ;
2026-01-06 22:57:00 -03:00
}
function getCellData ( row , col ) {
const ws = state . worksheets [ state . activeWorksheet ] ;
2026-01-11 09:56:44 -03:00
return ws ? . data [ ` ${ row } , ${ col } ` ] ;
2026-01-06 22:57:00 -03:00
}
function getCellValue ( row , col ) {
const data = getCellData ( row , col ) ;
2026-01-11 09:56:44 -03:00
if ( ! data ) return "" ;
if ( data . formula ) return evaluateFormula ( data . formula , row , col ) ;
return data . value || "" ;
2026-01-06 22:57:00 -03:00
}
function evaluateFormula ( formula , sourceRow , sourceCol ) {
if ( ! formula . startsWith ( "=" ) ) return formula ;
try {
2026-01-11 09:56:44 -03:00
let expr = formula . substring ( 1 ) . toUpperCase ( ) ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
expr = expr . replace ( /([A-Z]+)(\d+)/g , ( match , col , row ) => {
2026-01-06 22:57:00 -03:00
const r = parseInt ( row ) - 1 ;
2026-01-11 09:56:44 -03:00
const c = parseColName ( col ) ;
2026-01-06 22:57:00 -03:00
const val = getCellValue ( r , c ) ;
const num = parseFloat ( val ) ;
return isNaN ( num ) ? ` " ${ val } " ` : num ;
} ) ;
2026-01-11 09:56:44 -03:00
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 ) ;
}
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
const result = new Function ( "return " + expr ) ( ) ;
return typeof result === "number"
? Math . round ( result * 1000000 ) / 1000000
: result ;
2026-01-06 22:57:00 -03:00
} catch ( e ) {
2026-01-11 09:56:44 -03:00
return "#ERROR" ;
2026-01-06 22:57:00 -03:00
}
}
2026-01-11 09:56:44 -03:00
function evaluateSum ( expr ) {
const match = expr . match ( /SUM\(([^)]+)\)/i ) ;
if ( ! match ) return "#ERROR" ;
const values = parseRange ( match [ 1 ] ) ;
2026-01-06 22:57:00 -03:00
return values . reduce ( ( a , b ) => a + b , 0 ) ;
}
2026-01-11 09:56:44 -03:00
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 ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function evaluateCount ( expr ) {
const match = expr . match ( /COUNT\(([^)]+)\)/i ) ;
if ( ! match ) return "#ERROR" ;
const values = parseRange ( match [ 1 ] ) ;
2026-01-06 22:57:00 -03:00
return values . length ;
}
2026-01-11 09:56:44 -03:00
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 ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
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" ;
}
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
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 ;
}
2026-01-06 22:57:00 -03:00
function handleKeyDown ( e ) {
2026-01-11 09:56:44 -03:00
if ( e . target . closest ( ".chat-input, .modal input, .sheet-name-input" ) )
return ;
2026-01-06 22:57:00 -03:00
const { row , col } = state . activeCell ;
2026-01-11 09:56:44 -03:00
if ( e . ctrlKey || e . metaKey ) {
2026-01-06 22:57:00 -03:00
switch ( e . key . toLowerCase ( ) ) {
case "c" :
copySelection ( ) ;
2026-01-11 09:56:44 -03:00
return ;
2026-01-06 22:57:00 -03:00
case "x" :
cutSelection ( ) ;
2026-01-11 09:56:44 -03:00
return ;
2026-01-06 22:57:00 -03:00
case "v" :
pasteSelection ( ) ;
2026-01-11 09:56:44 -03:00
return ;
2026-01-06 22:57:00 -03:00
case "z" :
2026-01-11 09:56:44 -03:00
e . shiftKey ? redo ( ) : undo ( ) ;
2026-01-06 22:57:00 -03:00
e . preventDefault ( ) ;
2026-01-11 09:56:44 -03:00
return ;
2026-01-06 22:57:00 -03:00
case "y" :
redo ( ) ;
e . preventDefault ( ) ;
2026-01-11 09:56:44 -03:00
return ;
case "b" :
2026-01-06 22:57:00 -03:00
formatCells ( "bold" ) ;
e . preventDefault ( ) ;
2026-01-11 09:56:44 -03:00
return ;
case "i" :
2026-01-06 22:57:00 -03:00
formatCells ( "italic" ) ;
e . preventDefault ( ) ;
2026-01-11 09:56:44 -03:00
return ;
case "u" :
2026-01-06 22:57:00 -03:00
formatCells ( "underline" ) ;
e . preventDefault ( ) ;
2026-01-11 09:56:44 -03:00
return ;
2026-01-06 22:57:00 -03:00
case "a" :
selectAll ( ) ;
2026-01-11 09:56:44 -03:00
e . preventDefault ( ) ;
return ;
2026-01-06 22:57:00 -03:00
}
}
2026-01-11 09:56:44 -03:00
if ( state . isEditing ) return ;
2026-01-06 22:57:00 -03:00
switch ( e . key ) {
case "ArrowUp" :
2026-01-11 09:56:44 -03:00
navigateCell ( - 1 , 0 ) ;
2026-01-06 22:57:00 -03:00
e . preventDefault ( ) ;
break ;
case "ArrowDown" :
2026-01-11 09:56:44 -03:00
navigateCell ( 1 , 0 ) ;
2026-01-06 22:57:00 -03:00
e . preventDefault ( ) ;
break ;
case "ArrowLeft" :
2026-01-11 09:56:44 -03:00
navigateCell ( 0 , - 1 ) ;
2026-01-06 22:57:00 -03:00
e . preventDefault ( ) ;
break ;
case "ArrowRight" :
2026-01-11 09:56:44 -03:00
navigateCell ( 0 , 1 ) ;
2026-01-06 22:57:00 -03:00
e . preventDefault ( ) ;
break ;
2026-01-11 09:56:44 -03:00
case "Tab" :
navigateCell ( 0 , e . shiftKey ? - 1 : 1 ) ;
2026-01-06 22:57:00 -03:00
e . preventDefault ( ) ;
break ;
2026-01-11 09:56:44 -03:00
case "Enter" :
if ( e . shiftKey ) {
navigateCell ( - 1 , 0 ) ;
} else {
startEditing ( row , col ) ;
}
2026-01-06 22:57:00 -03:00
e . preventDefault ( ) ;
break ;
case "Delete" :
case "Backspace" :
clearCells ( ) ;
2026-01-11 09:56:44 -03:00
e . preventDefault ( ) ;
2026-01-06 22:57:00 -03:00
break ;
case "F2" :
startEditing ( row , col ) ;
2026-01-11 09:56:44 -03:00
e . preventDefault ( ) ;
2026-01-06 22:57:00 -03:00
break ;
default :
if ( e . key . length === 1 && ! e . ctrlKey && ! e . metaKey && ! e . altKey ) {
startEditing ( row , col ) ;
2026-01-11 09:56:44 -03:00
const cell = elements . cells . querySelector (
` [data-row=" ${ row } "][data-col=" ${ col } "] ` ,
) ;
const input = cell ? . querySelector ( ".cell-input" ) ;
if ( input ) input . value = e . key ;
2026-01-06 22:57:00 -03:00
}
}
}
2026-01-11 09:56:44 -03:00
function navigateCell ( dRow , dCol ) {
2026-01-06 22:57:00 -03:00
const newRow = Math . max (
0 ,
2026-01-11 09:56:44 -03:00
Math . min ( CONFIG . ROWS - 1 , state . activeCell . row + dRow ) ,
2026-01-06 22:57:00 -03:00
) ;
const newCol = Math . max (
0 ,
2026-01-11 09:56:44 -03:00
Math . min ( CONFIG . COLS - 1 , state . activeCell . col + dCol ) ,
2026-01-06 22:57:00 -03:00
) ;
selectCell ( newRow , newCol ) ;
}
function selectAll ( ) {
2026-01-11 09:56:44 -03:00
clearSelection ( ) ;
2026-01-06 22:57:00 -03:00
state . selection = {
start : { row : 0 , col : 0 } ,
end : { row : CONFIG . ROWS - 1 , col : CONFIG . COLS - 1 } ,
} ;
2026-01-11 09:56:44 -03:00
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" ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
updateSelectionInfo ( ) ;
}
2026-01-06 22:57:00 -03:00
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 ,
) ;
2026-01-11 09:56:44 -03:00
elements . calculationResult . textContent = ` = ${ result } ` ;
2026-01-06 22:57:00 -03:00
} else {
2026-01-11 09:56:44 -03:00
elements . calculationResult . textContent = "" ;
2026-01-06 22:57:00 -03:00
}
}
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 ) ;
2026-01-11 09:56:44 -03:00
elements . formulaInput . value = data ? . formula || data ? . value || "" ;
2026-01-06 22:57:00 -03:00
}
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 ;
2026-01-11 09:56:44 -03:00
if ( count === 1 ) {
elements . selectionInfo . textContent = "Ready" ;
2026-01-06 22:57:00 -03:00
} else {
2026-01-11 09:56:44 -03:00
elements . selectionInfo . textContent = ` ${ rows } R × ${ cols } C = ${ count } cells ` ;
2026-01-06 22:57:00 -03:00
}
}
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 ++ ) {
2026-01-11 09:56:44 -03:00
const val = parseFloat ( getCellValue ( r , c ) ) ;
if ( ! isNaN ( val ) ) values . push ( val ) ;
2026-01-06 22:57:00 -03:00
}
}
if ( values . length > 1 ) {
const sum = values . reduce ( ( a , b ) => a + b , 0 ) ;
const avg = sum / values . length ;
2026-01-11 09:56:44 -03:00
elements . calculationResult . textContent = ` Sum: ${ sum . toFixed ( 2 ) } | Avg: ${ avg . toFixed ( 2 ) } | Count: ${ values . length } ` ;
2026-01-06 22:57:00 -03:00
} else {
2026-01-11 09:56:44 -03:00
elements . calculationResult . textContent = "" ;
2026-01-06 22:57:00 -03:00
}
}
function copySelection ( ) {
state . clipboard = getSelectionData ( ) ;
state . clipboardMode = "copy" ;
showCopyBox ( ) ;
}
function cutSelection ( ) {
state . clipboard = getSelectionData ( ) ;
state . clipboardMode = "cut" ;
showCopyBox ( ) ;
}
function pasteSelection ( ) {
if ( ! state . clipboard ) return ;
2026-01-11 09:56:44 -03:00
saveToHistory ( ) ;
2026-01-06 22:57:00 -03:00
const { row , col } = state . activeCell ;
2026-01-11 09:56:44 -03:00
const ws = state . worksheets [ state . activeWorksheet ] ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
state . clipboard . forEach ( ( rowData , rOffset ) => {
rowData . forEach ( ( cellData , cOffset ) => {
const targetRow = row + rOffset ;
const targetCol = col + cOffset ;
const key = ` ${ targetRow } , ${ targetCol } ` ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
if ( cellData ) {
ws . data [ key ] = { ... cellData } ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
renderCell ( targetRow , targetCol ) ;
} ) ;
} ) ;
2026-01-06 22:57:00 -03:00
if ( state . clipboardMode === "cut" ) {
clearSourceCells ( ) ;
state . clipboardMode = null ;
}
2026-01-11 09:56:44 -03:00
hideCopyBox ( ) ;
2026-01-06 22:57:00 -03:00
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 ++ ) {
2026-01-11 09:56:44 -03:00
rowData . push ( getCellData ( r , c ) || null ) ;
2026-01-06 22:57:00 -03:00
}
data . push ( rowData ) ;
}
return data ;
}
function clearSourceCells ( ) {
const { start , end } = state . selection ;
2026-01-11 09:56:44 -03:00
const ws = state . worksheets [ state . activeWorksheet ] ;
2026-01-06 22:57:00 -03:00
for ( let r = start . row ; r <= end . row ; r ++ ) {
for ( let c = start . col ; c <= end . col ; c ++ ) {
2026-01-11 09:56:44 -03:00
delete ws . data [ ` ${ r } , ${ c } ` ] ;
2026-01-06 22:57:00 -03:00
renderCell ( r , c ) ;
}
}
}
function clearCells ( ) {
saveToHistory ( ) ;
2026-01-11 09:56:44 -03:00
const { start , end } = state . selection ;
const ws = state . worksheets [ state . activeWorksheet ] ;
2026-01-06 22:57:00 -03:00
for ( let r = start . row ; r <= end . row ; r ++ ) {
for ( let c = start . col ; c <= end . col ; c ++ ) {
2026-01-11 09:56:44 -03:00
delete ws . data [ ` ${ r } , ${ c } ` ] ;
2026-01-06 22:57:00 -03:00
renderCell ( r , c ) ;
}
}
2026-01-11 09:56:44 -03:00
state . isDirty = true ;
scheduleAutoSave ( ) ;
2026-01-06 22:57:00 -03:00
}
function showCopyBox ( ) {
2026-01-11 09:56:44 -03:00
const copyBox = document . getElementById ( "copyBox" ) ;
if ( copyBox ) copyBox . classList . remove ( "hidden" ) ;
2026-01-06 22:57:00 -03:00
}
function hideCopyBox ( ) {
2026-01-11 09:56:44 -03:00
const copyBox = document . getElementById ( "copyBox" ) ;
if ( copyBox ) copyBox . classList . add ( "hidden" ) ;
2026-01-06 22:57:00 -03:00
}
function formatCells ( format , value ) {
2026-01-11 09:56:44 -03:00
saveToHistory ( ) ;
2026-01-06 22:57:00 -03:00
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 } ` ;
2026-01-11 09:56:44 -03:00
if ( ! ws . data [ key ] ) ws . data [ key ] = { value : "" } ;
if ( ! ws . data [ key ] . style ) ws . data [ key ] . style = { } ;
const style = ws . data [ key ] . style ;
2026-01-06 22:57:00 -03:00
switch ( format ) {
case "bold" :
2026-01-11 09:56:44 -03:00
style . fontWeight = style . fontWeight === "bold" ? "normal" : "bold" ;
2026-01-06 22:57:00 -03:00
break ;
case "italic" :
2026-01-11 09:56:44 -03:00
style . fontStyle =
style . fontStyle === "italic" ? "normal" : "italic" ;
2026-01-06 22:57:00 -03:00
break ;
case "underline" :
2026-01-11 09:56:44 -03:00
style . textDecoration =
style . textDecoration === "underline" ? "none" : "underline" ;
2026-01-06 22:57:00 -03:00
break ;
case "strikethrough" :
2026-01-11 09:56:44 -03:00
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" ;
2026-01-06 22:57:00 -03:00
break ;
case "fontFamily" :
2026-01-11 09:56:44 -03:00
style . fontFamily = value ;
2026-01-06 22:57:00 -03:00
break ;
case "fontSize" :
2026-01-11 09:56:44 -03:00
style . fontSize = value ;
2026-01-06 22:57:00 -03:00
break ;
case "color" :
2026-01-11 09:56:44 -03:00
style . color = value ;
2026-01-06 22:57:00 -03:00
break ;
case "backgroundColor" :
2026-01-11 09:56:44 -03:00
style . background = value ;
2026-01-06 22:57:00 -03:00
break ;
2026-01-11 09:56:44 -03:00
case "currency" :
if ( ws . data [ key ] . value ) {
const num = parseFloat ( ws . data [ key ] . value ) ;
if ( ! isNaN ( num ) ) ws . data [ key ] . value = "$" + num . toFixed ( 2 ) ;
}
2026-01-06 22:57:00 -03:00
break ;
2026-01-11 09:56:44 -03:00
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 ) + "%" ;
}
2026-01-06 22:57:00 -03:00
break ;
}
renderCell ( r , c ) ;
}
}
state . isDirty = true ;
scheduleAutoSave ( ) ;
}
2026-01-11 09:56:44 -03:00
function mergeCells ( ) {
addChatMessage ( "assistant" , "Merge cells feature coming soon!" ) ;
}
2026-01-06 22:57:00 -03:00
function saveToHistory ( ) {
const snapshot = JSON . stringify ( state . worksheets ) ;
state . history = state . history . slice ( 0 , state . historyIndex + 1 ) ;
state . history . push ( snapshot ) ;
2026-01-11 09:56:44 -03:00
if ( state . history . length > CONFIG . MAX _HISTORY ) state . history . shift ( ) ;
2026-01-06 22:57:00 -03:00
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 ) {
2026-01-11 09:56:44 -03:00
const cell = e . target . closest ( ".cell" ) ;
if ( ! cell ) return ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
e . preventDefault ( ) ;
elements . contextMenu . style . left = e . clientX + "px" ;
elements . contextMenu . style . top = e . clientY + "px" ;
elements . contextMenu . classList . remove ( "hidden" ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function handleDocumentClick ( e ) {
if ( ! e . target . closest ( ".context-menu" ) ) {
elements . contextMenu ? . classList . add ( "hidden" ) ;
}
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
function handleContextAction ( action ) {
elements . contextMenu . classList . add ( "hidden" ) ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
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 ;
}
}
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
function insertRow ( atRow ) {
2026-01-06 22:57:00 -03:00
saveToHistory ( ) ;
2026-01-11 09:56:44 -03:00
const ws = state . worksheets [ state . activeWorksheet ] ;
const newData = { } ;
2026-01-06 22:57:00 -03:00
for ( const key in ws . data ) {
const [ r , c ] = key . split ( "," ) . map ( Number ) ;
2026-01-11 09:56:44 -03:00
if ( r >= atRow ) {
2026-01-06 22:57:00 -03:00
newData [ ` ${ r + 1 } , ${ c } ` ] = ws . data [ key ] ;
} else {
newData [ key ] = ws . data [ key ] ;
}
}
ws . data = newData ;
renderAllCells ( ) ;
state . isDirty = true ;
scheduleAutoSave ( ) ;
}
2026-01-11 09:56:44 -03:00
function insertColumn ( atCol ) {
saveToHistory ( ) ;
2026-01-06 22:57:00 -03:00
const ws = state . worksheets [ state . activeWorksheet ] ;
const newData = { } ;
for ( const key in ws . data ) {
const [ r , c ] = key . split ( "," ) . map ( Number ) ;
2026-01-11 09:56:44 -03:00
if ( c >= atCol ) {
2026-01-06 22:57:00 -03:00
newData [ ` ${ r } , ${ c + 1 } ` ] = ws . data [ key ] ;
} else {
newData [ key ] = ws . data [ key ] ;
}
}
ws . data = newData ;
renderAllCells ( ) ;
state . isDirty = true ;
scheduleAutoSave ( ) ;
}
2026-01-11 09:56:44 -03:00
function deleteRow ( row ) {
saveToHistory ( ) ;
2026-01-06 22:57:00 -03:00
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 ( ) ;
}
2026-01-11 09:56:44 -03:00
function deleteColumn ( col ) {
saveToHistory ( ) ;
2026-01-06 22:57:00 -03:00
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 ( ) ;
}
2026-01-11 09:56:44 -03:00
function clearFormatting ( ) {
2026-01-06 22:57:00 -03:00
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 ++ ) {
2026-01-11 09:56:44 -03:00
const key = ` ${ r } , ${ c } ` ;
if ( ws . data [ key ] ) {
delete ws . data [ key ] . style ;
renderCell ( r , c ) ;
}
2026-01-06 22:57:00 -03:00
}
}
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 ) ;
2026-01-11 09:56:44 -03:00
state . isDirty = true ;
scheduleAutoSave ( ) ;
2026-01-06 22:57:00 -03:00
}
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 ) => `
2026-01-11 09:56:44 -03:00
< div class = "sheet-tab ${i === state.activeWorksheet ? " active " : " "}" data - index = "${i}" >
< span > $ { escapeHtml ( ws . name ) } < / s p a n >
< button class = "tab-menu-btn" > ▼ < / b u t t o n >
< / d i v >
` ,
2026-01-06 22:57:00 -03:00
)
. join ( "" ) ;
2026-01-11 09:56:44 -03:00
elements . worksheetTabs . querySelectorAll ( ".sheet-tab" ) . forEach ( ( tab ) => {
tab . addEventListener ( "click" , ( ) =>
switchWorksheet ( parseInt ( tab . dataset . index ) ) ,
) ;
} ) ;
2026-01-06 22:57:00 -03:00
}
function zoomIn ( ) {
2026-01-11 09:56:44 -03:00
state . zoom = Math . min ( 200 , state . zoom + 10 ) ;
2026-01-06 22:57:00 -03:00
applyZoom ( ) ;
}
function zoomOut ( ) {
2026-01-11 09:56:44 -03:00
state . zoom = Math . max ( 50 , state . zoom - 10 ) ;
2026-01-06 22:57:00 -03:00
applyZoom ( ) ;
}
function applyZoom ( ) {
2026-01-11 09:56:44 -03:00
const scale = state . zoom / 100 ;
elements . cells . style . transform = ` scale( ${ scale } ) ` ;
2026-01-06 22:57:00 -03:00
elements . cells . style . transformOrigin = "top left" ;
2026-01-11 09:56:44 -03:00
elements . zoomLevel . textContent = state . zoom + "%" ;
2026-01-06 22:57:00 -03:00
}
function showModal ( id ) {
2026-01-11 09:56:44 -03:00
document . getElementById ( id ) ? . classList . remove ( "hidden" ) ;
2026-01-06 22:57:00 -03:00
}
function hideModal ( id ) {
2026-01-11 09:56:44 -03:00
document . getElementById ( id ) ? . classList . add ( "hidden" ) ;
2026-01-06 22:57:00 -03:00
}
function showShareModal ( ) {
2026-01-11 09:56:44 -03:00
const link = document . getElementById ( "shareLink" ) ;
if ( link ) link . value = window . location . href ;
showModal ( "shareModal" ) ;
2026-01-06 22:57:00 -03:00
}
function copyShareLink ( ) {
2026-01-11 09:56:44 -03:00
const input = document . getElementById ( "shareLink" ) ;
2026-01-06 22:57:00 -03:00
if ( input ) {
navigator . clipboard . writeText ( input . value ) ;
}
}
function scheduleAutoSave ( ) {
2026-01-11 09:56:44 -03:00
if ( state . autoSaveTimer ) clearTimeout ( state . autoSaveTimer ) ;
2026-01-06 22:57:00 -03:00
state . autoSaveTimer = setTimeout ( ( ) => {
2026-01-11 09:56:44 -03:00
if ( state . isDirty ) saveSheet ( ) ;
2026-01-06 22:57:00 -03:00
} , CONFIG . AUTOSAVE _DELAY ) ;
}
async function saveSheet ( ) {
2026-01-11 09:56:44 -03:00
elements . saveStatus . textContent = "Saving..." ;
2026-01-06 22:57:00 -03:00
try {
const response = await fetch ( "/api/sheet/save" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" } ,
2026-01-11 09:56:44 -03:00
body : JSON . stringify ( {
id : state . sheetId ,
name : state . sheetName ,
worksheets : state . worksheets ,
} ) ,
2026-01-06 22:57:00 -03:00
} ) ;
if ( response . ok ) {
const result = await response . json ( ) ;
if ( result . id ) {
state . sheetId = result . id ;
window . history . replaceState ( { } , "" , ` #id= ${ state . sheetId } ` ) ;
}
state . isDirty = false ;
2026-01-11 09:56:44 -03:00
elements . saveStatus . textContent = "Saved" ;
2026-01-06 22:57:00 -03:00
} else {
2026-01-11 09:56:44 -03:00
elements . saveStatus . textContent = "Save failed" ;
2026-01-06 22:57:00 -03:00
}
} catch ( e ) {
2026-01-11 09:56:44 -03:00
elements . saveStatus . textContent = "Save failed" ;
2026-01-06 22:57:00 -03:00
}
}
async function loadFromUrlParams ( ) {
const hash = window . location . hash ;
2026-01-11 09:56:44 -03:00
if ( ! hash ) return ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
const params = new URLSearchParams ( hash . substring ( 1 ) ) ;
const sheetId = params . get ( "id" ) ;
2026-01-06 22:57:00 -03:00
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 : { } } ] ;
2026-01-11 09:56:44 -03:00
if ( elements . sheetName ) elements . sheetName . value = state . sheetName ;
2026-01-06 22:57:00 -03:00
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 ) {
2026-01-11 09:56:44 -03:00
console . error ( "WebSocket failed:" , e ) ;
2026-01-06 22:57:00 -03:00
}
}
function handleWebSocketMessage ( msg ) {
switch ( msg . type ) {
case "cellChange" :
if ( msg . userId !== getUserId ( ) ) {
2026-01-11 09:56:44 -03:00
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 ] ;
}
2026-01-06 22:57:00 -03:00
renderCell ( msg . row , msg . col ) ;
}
break ;
case "cursor" :
updateRemoteCursor ( msg ) ;
break ;
case "userJoined" :
addCollaborator ( msg . user ) ;
break ;
case "userLeft" :
removeCollaborator ( msg . userId ) ;
break ;
}
}
2026-01-11 09:56:44 -03:00
function broadcastChange ( type , data ) {
if ( state . ws ? . readyState === WebSocket . OPEN ) {
2026-01-06 22:57:00 -03:00
state . ws . send (
JSON . stringify ( {
2026-01-11 09:56:44 -03:00
type ,
2026-01-06 22:57:00 -03:00
sheetId : state . sheetId ,
userId : getUserId ( ) ,
2026-01-11 09:56:44 -03:00
... data ,
2026-01-06 22:57:00 -03:00
} ) ,
) ;
}
}
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" ;
2026-01-11 09:56:44 -03:00
cursor . style . borderColor = msg . color || "#4285f4" ;
cursor . innerHTML = ` <div class="cursor-label" style="background: ${ msg . color || "#4285f4" } "> ${ escapeHtml ( msg . userName ) } </div> ` ;
elements . cursorIndicators ? . appendChild ( cursor ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
const cell = elements . cells . querySelector (
` [data-row=" ${ msg . row } "][data-col=" ${ msg . col } "] ` ,
) ;
if ( cell ) {
2026-01-06 22:57:00 -03:00
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 ) ;
2026-01-11 09:56:44 -03:00
document . getElementById ( ` cursor- ${ userId } ` ) ? . remove ( ) ;
2026-01-06 22:57:00 -03:00
renderCollaborators ( ) ;
}
function renderCollaborators ( ) {
elements . collaborators . innerHTML = state . collaborators
2026-01-11 09:56:44 -03:00
. slice ( 0 , 4 )
2026-01-06 22:57:00 -03:00
. map (
( u ) => `
2026-01-11 09:56:44 -03:00
< div class = "collaborator-avatar" style = "background:${u.color || " # 4285 f4 "}" title = "${escapeHtml(u.name)}" >
2026-01-06 22:57:00 -03:00
$ { u . name . charAt ( 0 ) . toUpperCase ( ) }
< / d i v >
` ,
)
. join ( "" ) ;
}
function getUserId ( ) {
2026-01-11 09:56:44 -03:00
let id = localStorage . getItem ( "gb-user-id" ) ;
2026-01-06 22:57:00 -03:00
if ( ! id ) {
id = "user-" + Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) ;
2026-01-11 09:56:44 -03:00
localStorage . setItem ( "gb-user-id" , id ) ;
2026-01-06 22:57:00 -03:00
}
return id ;
}
function getUserName ( ) {
2026-01-11 09:56:44 -03:00
return localStorage . getItem ( "gb-user-name" ) || "Anonymous" ;
2026-01-06 22:57:00 -03:00
}
function escapeHtml ( str ) {
if ( ! str ) return "" ;
return String ( str )
. replace ( /&/g , "&" )
. replace ( /</g , "<" )
2026-01-11 09:56:44 -03:00
. replace ( />/g , ">" ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
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 = ` <div class="message-bubble"> ${ escapeHtml ( content ) } </div> ` ;
elements . chatMessages . appendChild ( div ) ;
elements . chatMessages . scrollTop = elements . chatMessages . scrollHeight ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
async function processAICommand ( command ) {
const lower = command . toLowerCase ( ) ;
let response = "" ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
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" ;
}
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
addChatMessage ( "assistant" , response ) ;
2026-01-06 22:57:00 -03:00
}
2026-01-11 09:56:44 -03:00
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 ) ) ;
} ) ;
2026-01-06 22:57:00 -03:00
2026-01-11 09:56:44 -03:00
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
}
2026-01-06 22:57:00 -03:00
if ( document . readyState === "loading" ) {
document . addEventListener ( "DOMContentLoaded" , init ) ;
} else {
init ( ) ;
}
} ) ( ) ;