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-11 12:01:59 -03:00
findMatches : [ ] ,
findMatchIndex : - 1 ,
decimalPlaces : 2 ,
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 ( ) ;
2026-01-11 12:01:59 -03:00
renderCharts ( ) ;
renderImages ( ) ;
2026-01-06 22:57:00 -03:00
}
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-11 12:01:59 -03:00
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" ) ;
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
2026-01-11 12:01:59 -03:00
. getElementById ( "numberFormat" )
? . addEventListener ( "change" , handleNumberFormatChange ) ;
document
. getElementById ( "decreaseDecimalBtn" )
? . addEventListener ( "click" , decreaseDecimal ) ;
2026-01-11 09:56:44 -03:00
document
2026-01-11 12:01:59 -03:00
. getElementById ( "increaseDecimalBtn" )
? . addEventListener ( "click" , increaseDecimal ) ;
2026-01-11 09:56:44 -03:00
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 ) ;
2026-01-11 12:01:59 -03:00
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 ) ;
2026-01-11 09:56:44 -03:00
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 ( ) {
2026-01-11 12:01:59 -03:00
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!" ) ;
2026-01-11 09:56:44 -03:00
}
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
2026-01-11 12:01:59 -03:00
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 = "<table>" ;
if ( showHeaders ) {
html += "<thead><tr><th></th>" ;
for ( let c = 0 ; c < CONFIG . COLS ; c ++ ) {
html += ` <th> ${ getColName ( c ) } </th> ` ;
}
html += "</tr></thead>" ;
}
html += "<tbody>" ;
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 += "<tr>" ;
if ( showHeaders ) {
html += ` <th> ${ r + 1 } </th> ` ;
}
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 += ` <td style=" ${ styleStr } ${ borderStyle } "> ${ escapeHtml ( value ) } </td> ` ;
}
html += "</tr>" ;
}
html += "</tbody></table>" ;
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 ( `
< ! DOCTYPE html >
< html >
< head >
< title > $ { state . sheetName } < / t i t l e >
< style >
@ page { size : $ { orientation } ; margin : 0.5 in ; }
body { font - family : Arial , sans - serif ; font - size : 10 pt ; }
table { width : 100 % ; border - collapse : collapse ; }
td , th { border : 1 px solid # ccc ; padding : 4 px 8 px ; text - align : left ; }
th { background : # f5f5f5 ; font - weight : 600 ; }
< / s t y l e >
< / h e a d >
< body >
$ { printContent }
< / b o d y >
< / h t m l >
` );
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 `
< div class = "chart-wrapper" data - chart - id = "${id}" style = "left:${left}px;top:${top}px;width:${width}px;height:${height}px;" >
< div class = "chart-header" >
< h4 class = "chart-title" > $ { escapeHtml ( title || "Chart" ) } < / h 4 >
< div class = "chart-actions" >
< button class = "chart-delete" title = "Delete" > × < / b u t t o n >
< / d i v >
< / d i v >
< div class = "chart-content" >
$ { chartContent }
< / d i v >
$ { renderChartLegend ( data ) }
< / d i v >
` ;
}
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 '<div class="chart-empty">No data</div>' ;
const maxVal = Math . max ( ... data . values , 1 ) ;
const bars = data . values
. map ( ( val , i ) => {
const height = ( val / maxVal ) * maxHeight ;
return ` <div class="chart-bar" style="height: ${ height } px;" title=" ${ data . labels [ i ] } : ${ val } "></div> ` ;
} )
. join ( "" ) ;
return ` <div class="chart-bar-container" style="height: ${ maxHeight } px;"> ${ bars } </div> ` ;
}
function renderLineChart ( data , width , height ) {
if ( ! data . values . length ) return '<div class="chart-empty">No data</div>' ;
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 ` <circle class="chart-line-point" cx=" ${ x } " cy=" ${ y } " r="4"/> ` ;
} )
. join ( "" ) ;
return `
< svg class = "chart-canvas" viewBox = "0 0 ${width} ${height}" >
< polyline class = "chart-line" points = "${points.join(" ")}" / >
$ { circles }
< / s v g >
` ;
}
function renderPieChart ( data , size ) {
if ( ! data . values . length ) return '<div class="chart-empty">No data</div>' ;
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 ` <path d=" ${ path } " fill=" ${ colors [ i % colors . length ] } " stroke="white" stroke-width="2"/> ` ;
} )
. join ( "" ) ;
return `
< div class = "chart-pie-container" >
< svg class = "chart-canvas" viewBox = "0 0 ${size} ${size}" width = "${size}" height = "${size}" >
$ { slices }
< / s v g >
< / d i v >
` ;
}
function renderChartLegend ( data ) {
const colors = [
"#4285f4" ,
"#34a853" ,
"#fbbc04" ,
"#ea4335" ,
"#9c27b0" ,
"#00bcd4" ,
"#ff5722" ,
] ;
const items = data . labels
. map (
( label , i ) =>
` <div class="legend-item"><span class="legend-color" style="background: ${ colors [ i % colors . length ] } "></span> ${ escapeHtml ( label ) } </div> ` ,
)
. join ( "" ) ;
return ` <div class="chart-legend"> ${ items } </div> ` ;
}
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 `
< div class = "image-wrapper" data - image - id = "${img.id}" style = "left:${left}px;top:${top}px;width:${width}px;height:${height}px;" >
< img src = "${escapeHtml(img.url)}" alt = "Embedded image" / >
< div class = "image-resize-handle" > < / d i v >
< / d i v >
` ;
} )
. 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 ( ) ;
}
2026-01-06 22:57:00 -03:00
if ( document . readyState === "loading" ) {
document . addEventListener ( "DOMContentLoaded" , init ) ;
} else {
init ( ) ;
}
} ) ( ) ;