Spaces:
Running
Running
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Excel Clone Avanzado</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| .cell.selected { | |
| box-shadow: inset 0 0 0 2px #3b82f6; | |
| } | |
| .cell.editing { | |
| background-color: #1e293b; | |
| box-shadow: inset 0 0 0 2px #10b981; | |
| } | |
| .column-resize-handle { | |
| position: absolute; | |
| right: -2px; | |
| top: 0; | |
| width: 4px; | |
| height: 100%; | |
| background-color: transparent; | |
| cursor: col-resize; | |
| z-index: 10; | |
| } | |
| .row-resize-handle { | |
| position: absolute; | |
| bottom: -2px; | |
| left: 0; | |
| width: 100%; | |
| height: 4px; | |
| background-color: transparent; | |
| cursor: row-resize; | |
| z-index: 10; | |
| } | |
| .scroll-shadow-right { | |
| box-shadow: inset -10px 0 8px -8px rgba(0, 0, 0, 0.4); | |
| } | |
| .scroll-shadow-bottom { | |
| box-shadow: inset 0 -10px 8px -8px rgba(0, 0, 0, 0.4); | |
| } | |
| .tab-active { | |
| border-bottom: 2px solid #3b82f6; | |
| color: #3b82f6; | |
| } | |
| .format-button.active { | |
| background-color: #374151; | |
| } | |
| .color-preview { | |
| width: 16px; | |
| height: 16px; | |
| display: inline-block; | |
| border: 1px solid #6b7280; | |
| vertical-align: middle; | |
| margin-left: 4px; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-800 text-gray-100 h-screen flex flex-col overflow-hidden"> | |
| <!-- Toolbar Superior --> | |
| <div class="bg-gray-900 p-2 flex items-center border-b border-gray-700"> | |
| <!-- Archivo --> | |
| <div class="relative group"> | |
| <button id="file-menu" class="px-3 py-1 rounded hover:bg-gray-700 text-blue-400 font-medium"> | |
| <i class="fas fa-file mr-1"></i> Archivo | |
| </button> | |
| <div class="absolute hidden group-hover:block bg-gray-800 border border-gray-700 rounded shadow-lg z-20 w-48"> | |
| <button id="new-file" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-green-400"> | |
| <i class="fas fa-file-circle-plus mr-2"></i> Nuevo | |
| </button> | |
| <button id="open-file" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-yellow-400"> | |
| <i class="fas fa-folder-open mr-2"></i> Abrir | |
| </button> | |
| <button id="save-file" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-purple-400"> | |
| <i class="fas fa-save mr-2"></i> Guardar | |
| </button> | |
| <div class="border-t border-gray-700"></div> | |
| <button id="export-pdf" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-red-400"> | |
| <i class="fas fa-file-pdf mr-2"></i> Exportar PDF | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Edición --> | |
| <div class="relative group ml-2"> | |
| <button class="px-3 py-1 rounded hover:bg-gray-700 text-green-400 font-medium"> | |
| <i class="fas fa-edit mr-1"></i> Edición | |
| </button> | |
| <div class="absolute hidden group-hover:block bg-gray-800 border border-gray-700 rounded shadow-lg z-20 w-48"> | |
| <button id="undo" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-blue-400"> | |
| <i class="fas fa-undo mr-2"></i> Deshacer | |
| </button> | |
| <button id="redo" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-purple-400"> | |
| <i class="fas fa-redo mr-2"></i> Rehacer | |
| </button> | |
| <div class="border-t border-gray-700"></div> | |
| <button id="cut" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-yellow-400"> | |
| <i class="fas fa-cut mr-2"></i> Cortar | |
| </button> | |
| <button id="copy" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-pink-400"> | |
| <i class="fas fa-copy mr-2"></i> Copiar | |
| </button> | |
| <button id="paste" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-indigo-400"> | |
| <i class="fas fa-paste mr-2"></i> Pegar | |
| </button> | |
| <div class="border-t border-gray-700"></div> | |
| <button id="clear-contents" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-red-400"> | |
| <i class="fas fa-eraser mr-2"></i> Limpiar contenido | |
| </button> | |
| <button id="clear-formats" class="block w-full text-left px-4 py-2 hover:bg-gray-700 text-orange-400"> | |
| <i class="fas fa-remove-format mr-2"></i> Limpiar formato | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Formato --> | |
| <div class="flex items-center space-x-2 ml-4"> | |
| <select id="font-family" class="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-white hover:bg-gray-700"> | |
| <option>Calibri</option> | |
| <option>Arial</option> | |
| <option>Times New Roman</option> | |
| <option>Courier New</option> | |
| <option>Verdana</option> | |
| </select> | |
| <select id="font-size" class="bg-gray-800 border border-gray-700 rounded px-2 py-1 text-sm text-white hover:bg-gray-700"> | |
| <option>8</option> | |
| <option>10</option> | |
| <option>11</option> | |
| <option selected>12</option> | |
| <option>14</option> | |
| <option>16</option> | |
| <option>18</option> | |
| <option>20</option> | |
| <option>24</option> | |
| </select> | |
| <button id="bold" class="p-2 rounded hover:bg-gray-700 text-pink-400 format-button"> | |
| <i class="fas fa-bold"></i> | |
| </button> | |
| <button id="italic" class="p-2 rounded hover:bg-gray-700 text-red-400 format-button"> | |
| <i class="fas fa-italic"></i> | |
| </button> | |
| <button id="underline" class="p-2 rounded hover:bg-gray-700 text-orange-400 format-button"> | |
| <i class="fas fa-underline"></i> | |
| </button> | |
| <div class="border-l border-gray-700 h-6 mx-1"></div> | |
| <button id="align-left" class="p-2 rounded hover:bg-gray-700 text-cyan-400 format-button"> | |
| <i class="fas fa-align-left"></i> | |
| </button> | |
| <button id="align-center" class="p-2 rounded hover:bg-gray-700 text-emerald-400 format-button"> | |
| <i class="fas fa-align-center"></i> | |
| </button> | |
| <button id="align-right" class="p-2 rounded hover:bg-gray-700 text-blue-400 format-button"> | |
| <i class="fas fa-align-right"></i> | |
| </button> | |
| <div class="border-l border-gray-700 h-6 mx-1"></div> | |
| <div class="relative"> | |
| <button id="text-color" class="p-2 rounded hover:bg-gray-700 text-yellow-400 flex items-center"> | |
| <i class="fas fa-font"></i> | |
| <span id="text-color-preview" class="color-preview"></span> | |
| </button> | |
| </div> | |
| <div class="relative"> | |
| <button id="fill-color" class="p-2 rounded hover:bg-gray-700 text-green-400 flex items-center"> | |
| <i class="fas fa-fill-drip"></i> | |
| <span id="fill-color-preview" class="color-preview"></span> | |
| </button> | |
| </div> | |
| <button id="border-style" class="p-2 rounded hover:bg-gray-700 text-purple-400"> | |
| <i class="fas fa-border-all"></i> | |
| </button> | |
| <button id="remove-colors" class="p-2 rounded hover:bg-gray-700 text-red-400"> | |
| <i class="fas fa-palette"></i> <i class="fas fa-slash"></i> | |
| </button> | |
| </div> | |
| <!-- Fórmulas y Datos --> | |
| <div class="ml-auto flex items-center space-x-2"> | |
| <button id="insert-function" class="px-3 py-1 rounded hover:bg-gray-700 text-indigo-400"> | |
| <i class="fas fa-sigma mr-1"></i> Fx | |
| </button> | |
| <button id="sort-asc" class="p-2 rounded hover:bg-gray-700 text-blue-400"> | |
| <i class="fas fa-sort-amount-up"></i> | |
| </button> | |
| <button id="sort-desc" class="p-2 rounded hover:bg-gray-700 text-red-400"> | |
| <i class="fas fa-sort-amount-down"></i> | |
| </button> | |
| <button id="filter" class="p-2 rounded hover:bg-gray-700 text-rose-400"> | |
| <i class="fas fa-filter"></i> | |
| </button> | |
| <button id="chart" class="p-2 rounded hover:bg-gray-700 text-green-400"> | |
| <i class="fas fa-chart-line"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Pestañas de Hojas --> | |
| <div class="bg-gray-900 p-1 flex items-center border-b border-gray-700"> | |
| <button id="add-sheet" class="px-2 py-1 rounded hover:bg-gray-700 text-green-400"> | |
| <i class="fas fa-plus"></i> | |
| </button> | |
| <div class="flex ml-2 overflow-x-auto"> | |
| <button class="px-3 py-1 rounded-l hover:bg-gray-700 text-sm font-medium tab-active"> | |
| Hoja1 | |
| </button> | |
| <button class="px-3 py-1 hover:bg-gray-700 text-sm font-medium text-gray-400"> | |
| Hoja2 | |
| </button> | |
| <button class="px-3 py-1 rounded-r hover:bg-gray-700 text-sm font-medium text-gray-400"> | |
| Hoja3 | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Formula Bar --> | |
| <div class="bg-gray-900 p-1 flex items-center border-b border-gray-700"> | |
| <div id="cell-reference" class="w-16 text-center text-sm font-mono text-blue-400">A1</div> | |
| <div class="flex-1 flex"> | |
| <input type="text" id="formula-input" class="bg-gray-800 border border-gray-700 rounded-l px-2 py-1 text-sm w-full font-mono focus:outline-none focus:ring-1 focus:ring-blue-500" placeholder="Introduce fórmula..."> | |
| <button id="apply-formula" class="bg-blue-600 hover:bg-blue-700 px-3 rounded-r"> | |
| <i class="fas fa-check"></i> | |
| </button> | |
| </div> | |
| <div class="ml-2 text-xs text-gray-400"> | |
| <span id="formula-help">F2 para editar celda, Enter para confirmar</span> | |
| </div> | |
| </div> | |
| <!-- Main Grid --> | |
| <div class="flex-1 flex overflow-hidden"> | |
| <!-- Row Numbers --> | |
| <div class="bg-gray-900 border-r border-gray-700 overflow-hidden" id="row-numbers"> | |
| <div class="w-10 h-8 flex items-center justify-center text-xs text-gray-400 border-b border-gray-700"></div> | |
| <!-- Rows will be added dynamically --> | |
| </div> | |
| <!-- Column Headers + Grid --> | |
| <div class="flex-1 flex flex-col overflow-hidden"> | |
| <!-- Column Headers --> | |
| <div class="bg-gray-900 border-b border-gray-700 flex" id="column-headers"> | |
| <!-- Columns will be added dynamically --> | |
| </div> | |
| <!-- Grid Container --> | |
| <div class="flex-1 overflow-auto relative scroll-shadow-right scroll-shadow-bottom" id="grid-container"> | |
| <div id="grid" class="absolute"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Status Bar --> | |
| <div class="bg-gray-900 p-1 flex items-center border-t border-gray-700 text-xs"> | |
| <div class="flex space-x-4"> | |
| <span id="status" class="text-green-400">Listo</span> | |
| <span id="num-lock" class="text-yellow-400 hidden">NUM</span> | |
| <span id="caps-lock" class="text-blue-400 hidden">MAYÚS</span> | |
| <span id="selection-info" class="text-purple-400">1 celda seleccionada</span> | |
| </div> | |
| <div class="ml-auto flex space-x-2"> | |
| <span class="text-pink-400">Zoom: 100%</span> | |
| <button id="zoom-out" class="text-pink-400 hover:bg-gray-700 px-1 rounded">-</button> | |
| <button id="zoom-in" class="text-pink-400 hover:bg-gray-700 px-1 rounded">+</button> | |
| <button id="zoom-reset" class="text-pink-400 hover:bg-gray-700 px-1 rounded">100%</button> | |
| </div> | |
| </div> | |
| <!-- Color Picker (hidden by default) --> | |
| <div id="color-picker" class="hidden absolute bg-gray-800 border border-gray-700 rounded shadow-lg p-2 z-50"> | |
| <div class="grid grid-cols-8 gap-1"> | |
| <div class="w-4 h-4 bg-red-500 hover:border hover:border-white cursor-pointer" data-color="#ef4444"></div> | |
| <div class="w-4 h-4 bg-orange-500 hover:border hover:border-white cursor-pointer" data-color="#f97316"></div> | |
| <div class="w-4 h-4 bg-yellow-500 hover:border hover:border-white cursor-pointer" data-color="#eab308"></div> | |
| <div class="w-4 h-4 bg-green-500 hover:border hover:border-white cursor-pointer" data-color="#22c55e"></div> | |
| <div class="w-4 h-4 bg-emerald-500 hover:border hover:border-white cursor-pointer" data-color="#10b981"></div> | |
| <div class="w-4 h-4 bg-cyan-500 hover:border hover:border-white cursor-pointer" data-color="#06b6d4"></div> | |
| <div class="w-4 h-4 bg-blue-500 hover:border hover:border-white cursor-pointer" data-color="#3b82f6"></div> | |
| <div class="w-4 h-4 bg-indigo-500 hover:border hover:border-white cursor-pointer" data-color="#6366f1"></div> | |
| <div class="w-4 h-4 bg-purple-500 hover:border hover:border-white cursor-pointer" data-color="#8b5cf6"></div> | |
| <div class="w-4 h-4 bg-pink-500 hover:border hover:border-white cursor-pointer" data-color="#ec4899"></div> | |
| <div class="w-4 h-4 bg-rose-500 hover:border hover:border-white cursor-pointer" data-color="#f43f5e"></div> | |
| <div class="w-4 h-4 bg-amber-500 hover:border hover:border-white cursor-pointer" data-color="#f59e0b"></div> | |
| <div class="w-4 h-4 bg-lime-500 hover:border hover:border-white cursor-pointer" data-color="#84cc16"></div> | |
| <div class="w-4 h-4 bg-teal-500 hover:border hover:border-white cursor-pointer" data-color="#14b8a6"></div> | |
| <div class="w-4 h-4 bg-sky-500 hover:border hover:border-white cursor-pointer" data-color="#0ea5e9"></div> | |
| <div class="w-4 h-4 bg-violet-500 hover:border hover:border-white cursor-pointer" data-color="#8b5cf6"></div> | |
| </div> | |
| <div class="mt-2 flex justify-between"> | |
| <button id="color-picker-cancel" class="px-2 py-1 text-xs bg-gray-700 rounded hover:bg-gray-600">Cancelar</button> | |
| <button id="color-picker-apply" class="px-2 py-1 text-xs bg-blue-600 rounded hover:bg-blue-700">Aplicar</button> | |
| <button id="color-picker-remove" class="px-2 py-1 text-xs bg-red-600 rounded hover:bg-red-700">Quitar</button> | |
| </div> | |
| </div> | |
| <script> | |
| document.addEventListener('DOMContentLoaded', function() { | |
| // Configuration | |
| const ROWS = 50; | |
| const COLS = 26; | |
| const CELL_WIDTH = 100; | |
| const CELL_HEIGHT = 25; | |
| const HEADER_HEIGHT = 25; | |
| // State | |
| let selectedCell = null; | |
| let editingCell = null; | |
| let data = {}; | |
| let formulas = {}; | |
| let cellWidths = {}; | |
| let cellHeights = {}; | |
| let cellStyles = {}; | |
| let activeSheet = 'Hoja1'; | |
| let sheets = { | |
| 'Hoja1': { data: {}, formulas: {}, cellStyles: {} }, | |
| 'Hoja2': { data: {}, formulas: {}, cellStyles: {} }, | |
| 'Hoja3': { data: {}, formulas: {}, cellStyles: {} } | |
| }; | |
| let history = []; | |
| let historyIndex = -1; | |
| let isBold = false; | |
| let isItalic = false; | |
| let isUnderline = false; | |
| let textAlign = 'left'; | |
| let currentColorPickerType = null; | |
| let defaultTextColor = '#ffffff'; // Blanco | |
| let defaultFillColor = 'transparent'; // Transparente | |
| // DOM Elements | |
| const grid = document.getElementById('grid'); | |
| const gridContainer = document.getElementById('grid-container'); | |
| const columnHeaders = document.getElementById('column-headers'); | |
| const rowNumbers = document.getElementById('row-numbers'); | |
| const formulaInput = document.getElementById('formula-input'); | |
| const cellReference = document.getElementById('cell-reference'); | |
| const statusElement = document.getElementById('status'); | |
| const selectionInfo = document.getElementById('selection-info'); | |
| const colorPicker = document.getElementById('color-picker'); | |
| const numLockElement = document.getElementById('num-lock'); | |
| const capsLockElement = document.getElementById('caps-lock'); | |
| const textColorPreview = document.getElementById('text-color-preview'); | |
| const fillColorPreview = document.getElementById('fill-color-preview'); | |
| // Initialize grid | |
| function initGrid() { | |
| // Set grid dimensions | |
| grid.style.width = `${COLS * CELL_WIDTH}px`; | |
| grid.style.height = `${ROWS * CELL_HEIGHT}px`; | |
| // Create column headers (A, B, C...) | |
| for (let col = 0; col < COLS; col++) { | |
| const colHeader = document.createElement('div'); | |
| const colLetter = String.fromCharCode(65 + col); | |
| colHeader.className = 'h-8 flex items-center justify-center text-xs text-gray-400 border-b border-gray-700 relative'; | |
| colHeader.style.width = `${CELL_WIDTH}px`; | |
| colHeader.textContent = colLetter; | |
| // Add resize handle | |
| const resizeHandle = document.createElement('div'); | |
| resizeHandle.className = 'column-resize-handle'; | |
| resizeHandle.dataset.col = col; | |
| resizeHandle.addEventListener('mousedown', startColumnResize); | |
| colHeader.appendChild(resizeHandle); | |
| columnHeaders.appendChild(colHeader); | |
| } | |
| // Create row numbers (1, 2, 3...) | |
| for (let row = 1; row <= ROWS; row++) { | |
| const rowNumber = document.createElement('div'); | |
| rowNumber.className = 'w-10 h-8 flex items-center justify-center text-xs text-gray-400 border-b border-gray-700 relative'; | |
| rowNumber.textContent = row; | |
| // Add resize handle | |
| const resizeHandle = document.createElement('div'); | |
| resizeHandle.className = 'row-resize-handle'; | |
| resizeHandle.dataset.row = row - 1; | |
| resizeHandle.addEventListener('mousedown', startRowResize); | |
| rowNumber.appendChild(resizeHandle); | |
| rowNumbers.appendChild(rowNumber); | |
| } | |
| // Create cells | |
| for (let row = 0; row < ROWS; row++) { | |
| for (let col = 0; col < COLS; col++) { | |
| const cell = document.createElement('div'); | |
| const cellId = `${String.fromCharCode(65 + col)}${row + 1}`; | |
| cell.className = 'cell absolute bg-gray-800 border border-gray-700 p-1 overflow-hidden text-sm'; | |
| cell.style.width = `${CELL_WIDTH}px`; | |
| cell.style.height = `${CELL_HEIGHT}px`; | |
| cell.style.left = `${col * CELL_WIDTH}px`; | |
| cell.style.top = `${row * CELL_HEIGHT}px`; | |
| cell.dataset.row = row; | |
| cell.dataset.col = col; | |
| cell.dataset.id = cellId; | |
| cell.addEventListener('click', handleCellClick); | |
| cell.addEventListener('dblclick', handleCellDoubleClick); | |
| grid.appendChild(cell); | |
| } | |
| } | |
| // Set default color previews | |
| textColorPreview.style.backgroundColor = defaultTextColor; | |
| fillColorPreview.style.backgroundColor = defaultFillColor; | |
| // Add initial state to history | |
| saveState(); | |
| } | |
| // Handle cell click | |
| function handleCellClick(e) { | |
| const cell = e.currentTarget; | |
| const cellId = cell.dataset.id; | |
| // Remove previous selection | |
| if (selectedCell) { | |
| selectedCell.classList.remove('selected'); | |
| } | |
| // Set new selection | |
| selectedCell = cell; | |
| cell.classList.add('selected'); | |
| // Update formula bar | |
| formulaInput.value = formulas[cellId] || data[cellId] || ''; | |
| formulaInput.focus(); | |
| // Update cell reference | |
| cellReference.textContent = cellId; | |
| // Update selection info | |
| selectionInfo.textContent = "1 celda seleccionada"; | |
| // Update status | |
| statusElement.textContent = "Listo"; | |
| // Update format buttons based on cell style | |
| updateFormatButtons(cellId); | |
| } | |
| // Update format buttons based on cell style | |
| function updateFormatButtons(cellId) { | |
| // Reset all format buttons | |
| document.querySelectorAll('.format-button').forEach(btn => btn.classList.remove('active')); | |
| if (cellStyles[cellId]) { | |
| const style = cellStyles[cellId]; | |
| // Bold | |
| if (style.bold) { | |
| document.getElementById('bold').classList.add('active'); | |
| } | |
| // Italic | |
| if (style.italic) { | |
| document.getElementById('italic').classList.add('active'); | |
| } | |
| // Underline | |
| if (style.underline) { | |
| document.getElementById('underline').classList.add('active'); | |
| } | |
| // Text align | |
| if (style.textAlign) { | |
| document.getElementById(`align-${style.textAlign}`).classList.add('active'); | |
| } | |
| // Update color previews | |
| if (style.color) { | |
| textColorPreview.style.backgroundColor = style.color; | |
| } else { | |
| textColorPreview.style.backgroundColor = defaultTextColor; | |
| } | |
| if (style.backgroundColor) { | |
| fillColorPreview.style.backgroundColor = style.backgroundColor; | |
| } else { | |
| fillColorPreview.style.backgroundColor = defaultFillColor; | |
| } | |
| } | |
| } | |
| // Handle cell double click (edit mode) | |
| function handleCellDoubleClick(e) { | |
| const cell = e.currentTarget; | |
| const cellId = cell.dataset.id; | |
| // Exit previous edit mode | |
| if (editingCell) { | |
| exitEditMode(); | |
| } | |
| // Enter edit mode | |
| editingCell = cell; | |
| cell.classList.add('editing'); | |
| // Create input element | |
| const input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.className = 'w-full h-full bg-gray-800 text-white px-1 outline-none'; | |
| input.value = data[cellId] || ''; | |
| input.dataset.cellId = cellId; | |
| // Apply cell styles to input | |
| if (cellStyles[cellId]) { | |
| if (cellStyles[cellId].bold) input.style.fontWeight = 'bold'; | |
| if (cellStyles[cellId].italic) input.style.fontStyle = 'italic'; | |
| if (cellStyles[cellId].underline) input.style.textDecoration = 'underline'; | |
| if (cellStyles[cellId].textAlign) input.style.textAlign = cellStyles[cellId].textAlign; | |
| if (cellStyles[cellId].color) input.style.color = cellStyles[cellId].color; | |
| if (cellStyles[cellId].backgroundColor) input.style.backgroundColor = cellStyles[cellId].backgroundColor; | |
| if (cellStyles[cellId].fontSize) input.style.fontSize = cellStyles[cellId].fontSize + 'px'; | |
| if (cellStyles[cellId].fontFamily) input.style.fontFamily = cellStyles[cellId].fontFamily; | |
| } | |
| // Clear cell content and add input | |
| cell.innerHTML = ''; | |
| cell.appendChild(input); | |
| input.focus(); | |
| // Handle input events | |
| input.addEventListener('blur', handleInputBlur); | |
| input.addEventListener('keydown', handleInputKeyDown); | |
| // Update status | |
| statusElement.textContent = "Editando"; | |
| } | |
| // Exit edit mode | |
| function exitEditMode() { | |
| if (!editingCell) return; | |
| const cell = editingCell; | |
| const cellId = cell.dataset.id; | |
| const input = cell.querySelector('input'); | |
| if (input) { | |
| // Save data | |
| const value = input.value.trim(); | |
| if (value.startsWith('=')) { | |
| formulas[cellId] = value; | |
| try { | |
| data[cellId] = evaluateFormula(value.substring(1)); | |
| } catch (e) { | |
| data[cellId] = '#ERROR!'; | |
| } | |
| } else { | |
| data[cellId] = value; | |
| delete formulas[cellId]; | |
| } | |
| // Update cell display | |
| cell.innerHTML = ''; | |
| cell.textContent = data[cellId] || ''; | |
| // Apply styles | |
| applyCellStyles(cell, cellId); | |
| } | |
| cell.classList.remove('editing'); | |
| editingCell = null; | |
| // Save state to history | |
| saveState(); | |
| // Update status | |
| statusElement.textContent = "Listo"; | |
| } | |
| // Apply cell styles | |
| function applyCellStyles(cell, cellId) { | |
| // Reset to defaults first | |
| cell.style = ''; | |
| cell.className = 'cell absolute bg-gray-800 border border-gray-700 p-1 overflow-hidden text-sm'; | |
| if (cellStyles[cellId]) { | |
| const style = cellStyles[cellId]; | |
| if (style.bold) cell.style.fontWeight = 'bold'; | |
| if (style.italic) cell.style.fontStyle = 'italic'; | |
| if (style.underline) cell.style.textDecoration = 'underline'; | |
| if (style.textAlign) cell.style.textAlign = style.textAlign; | |
| if (style.color) cell.style.color = style.color; | |
| if (style.backgroundColor) cell.style.backgroundColor = style.backgroundColor; | |
| if (style.fontSize) cell.style.fontSize = style.fontSize + 'px'; | |
| if (style.fontFamily) cell.style.fontFamily = style.fontFamily; | |
| } | |
| // Update color previews | |
| updateColorPreviews(cellId); | |
| } | |
| // Update color previews | |
| function updateColorPreviews(cellId) { | |
| if (cellStyles[cellId]) { | |
| if (cellStyles[cellId].color) { | |
| textColorPreview.style.backgroundColor = cellStyles[cellId].color; | |
| } else { | |
| textColorPreview.style.backgroundColor = defaultTextColor; | |
| } | |
| if (cellStyles[cellId].backgroundColor) { | |
| fillColorPreview.style.backgroundColor = cellStyles[cellId].backgroundColor; | |
| } else { | |
| fillColorPreview.style.backgroundColor = defaultFillColor; | |
| } | |
| } else { | |
| textColorPreview.style.backgroundColor = defaultTextColor; | |
| fillColorPreview.style.backgroundColor = defaultFillColor; | |
| } | |
| } | |
| // Handle input blur (exit edit mode) | |
| function handleInputBlur(e) { | |
| exitEditMode(); | |
| } | |
| // Handle input key events | |
| function handleInputKeyDown(e) { | |
| if (e.key === 'Enter') { | |
| exitEditMode(); | |
| } else if (e.key === 'Escape') { | |
| const input = e.currentTarget; | |
| const cell = input.parentElement; | |
| // Restore original content | |
| cell.innerHTML = ''; | |
| cell.textContent = data[cell.dataset.id] || ''; | |
| applyCellStyles(cell, cell.dataset.id); | |
| cell.classList.remove('editing'); | |
| editingCell = null; | |
| // Update status | |
| statusElement.textContent = "Listo"; | |
| } | |
| } | |
| // Simple formula evaluation (very basic implementation) | |
| function evaluateFormula(formula) { | |
| // Basic SUM implementation | |
| if (formula.toUpperCase().startsWith('SUM(')) { | |
| const range = formula.match(/\((.*?)\)/)[1]; | |
| const [start, end] = range.split(':'); | |
| let sum = 0; | |
| const startCol = start.charCodeAt(0) - 65; | |
| const startRow = parseInt(start.substring(1)) - 1; | |
| const endCol = end.charCodeAt(0) - 65; | |
| const endRow = parseInt(end.substring(1)) - 1; | |
| for (let row = startRow; row <= endRow; row++) { | |
| for (let col = startCol; col <= endCol; col++) { | |
| const cellId = `${String.fromCharCode(65 + col)}${row + 1}`; | |
| const value = parseFloat(data[cellId]) || 0; | |
| sum += value; | |
| } | |
| } | |
| return sum; | |
| } | |
| // Basic arithmetic | |
| if (formula.includes('+')) { | |
| const parts = formula.split('+'); | |
| return (parseFloat(parts[0]) || 0) + (parseFloat(parts[1]) || 0); | |
| } | |
| if (formula.includes('-')) { | |
| const parts = formula.split('-'); | |
| return (parseFloat(parts[0]) || 0) - (parseFloat(parts[1]) || 0); | |
| } | |
| if (formula.includes('*')) { | |
| const parts = formula.split('*'); | |
| return (parseFloat(parts[0]) || 0) * (parseFloat(parts[1]) || 0); | |
| } | |
| if (formula.includes('/')) { | |
| const parts = formula.split('/'); | |
| return (parseFloat(parts[0]) || 0) / (parseFloat(parts[1]) || 1); | |
| } | |
| // Cell reference | |
| if (/^[A-Z]+\d+$/.test(formula)) { | |
| return data[formula] || 0; | |
| } | |
| return formula; // Return as is if not a recognized formula | |
| } | |
| // Handle column resize | |
| let isResizingColumn = false; | |
| let resizingCol = null; | |
| let startX = 0; | |
| let startWidth = 0; | |
| function startColumnResize(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| isResizingColumn = true; | |
| resizingCol = parseInt(e.currentTarget.dataset.col); | |
| startX = e.clientX; | |
| startWidth = CELL_WIDTH; | |
| document.addEventListener('mousemove', handleColumnResize); | |
| document.addEventListener('mouseup', stopColumnResize); | |
| // Update status | |
| statusElement.textContent = "Redimensionando columna"; | |
| } | |
| function handleColumnResize(e) { | |
| if (!isResizingColumn) return; | |
| const dx = e.clientX - startX; | |
| const newWidth = Math.max(30, startWidth + dx); | |
| // Update column width | |
| const colLetter = String.fromCharCode(65 + resizingCol); | |
| cellWidths[colLetter] = newWidth; | |
| // Update header | |
| columnHeaders.children[resizingCol].style.width = `${newWidth}px`; | |
| // Update all cells in this column | |
| const cells = document.querySelectorAll(`.cell[data-col="${resizingCol}"]`); | |
| cells.forEach(cell => { | |
| cell.style.width = `${newWidth}px`; | |
| }); | |
| // Update grid width | |
| updateGridDimensions(); | |
| } | |
| function stopColumnResize() { | |
| isResizingColumn = false; | |
| document.removeEventListener('mousemove', handleColumnResize); | |
| document.removeEventListener('mouseup', stopColumnResize); | |
| // Save state to history | |
| saveState(); | |
| // Update status | |
| statusElement.textContent = "Listo"; | |
| } | |
| // Handle row resize | |
| let isResizingRow = false; | |
| let resizingRow = null; | |
| let startY = 0; | |
| let startHeight = 0; | |
| function startRowResize(e) { | |
| e.preventDefault(); | |
| e.stopPropagation(); | |
| isResizingRow = true; | |
| resizingRow = parseInt(e.currentTarget.dataset.row); | |
| startY = e.clientY; | |
| startHeight = CELL_HEIGHT; | |
| document.addEventListener('mousemove', handleRowResize); | |
| document.addEventListener('mouseup', stopRowResize); | |
| // Update status | |
| statusElement.textContent = "Redimensionando fila"; | |
| } | |
| function handleRowResize(e) { | |
| if (!isResizingRow) return; | |
| const dy = e.clientY - startY; | |
| const newHeight = Math.max(20, startHeight + dy); | |
| // Update row height | |
| cellHeights[resizingRow + 1] = newHeight; | |
| // Update row number display | |
| rowNumbers.children[resizingRow + 1].style.height = `${newHeight}px`; | |
| // Update all cells in this row | |
| const cells = document.querySelectorAll(`.cell[data-row="${resizingRow}"]`); | |
| cells.forEach(cell => { | |
| cell.style.height = `${newHeight}px`; | |
| }); | |
| // Update grid height | |
| updateGridDimensions(); | |
| } | |
| function stopRowResize() { | |
| isResizingRow = false; | |
| document.removeEventListener('mousemove', handleRowResize); | |
| document.removeEventListener('mouseup', stopRowResize); | |
| // Save state to history | |
| saveState(); | |
| // Update status | |
| statusElement.textContent = "Listo"; | |
| } | |
| // Update grid dimensions after resize | |
| function updateGridDimensions() { | |
| let totalWidth = 0; | |
| for (let col = 0; col < COLS; col++) { | |
| const colLetter = String.fromCharCode(65 + col); | |
| totalWidth += cellWidths[colLetter] || CELL_WIDTH; | |
| } | |
| let totalHeight = 0; | |
| for (let row = 0; row < ROWS; row++) { | |
| totalHeight += cellHeights[row + 1] || CELL_HEIGHT; | |
| } | |
| grid.style.width = `${totalWidth}px`; | |
| grid.style.height = `${totalHeight}px`; | |
| } | |
| // Handle formula input | |
| formulaInput.addEventListener('keydown', function(e) { | |
| if (e.key === 'Enter' && selectedCell) { | |
| const value = formulaInput.value.trim(); | |
| const cellId = selectedCell.dataset.id; | |
| if (value.startsWith('=')) { | |
| formulas[cellId] = value; | |
| try { | |
| data[cellId] = evaluateFormula(value.substring(1)); | |
| } catch (e) { | |
| data[cellId] = '#ERROR!'; | |
| } | |
| } else { | |
| data[cellId] = value; | |
| delete formulas[cellId]; | |
| } | |
| selectedCell.textContent = data[cellId] || ''; | |
| formulaInput.blur(); | |
| // Save state to history | |
| saveState(); | |
| } | |
| }); | |
| // Apply formula button | |
| document.getElementById('apply-formula').addEventListener('click', function() { | |
| if (selectedCell) { | |
| const value = formulaInput.value.trim(); | |
| const cellId = selectedCell.dataset.id; | |
| if (value.startsWith('=')) { | |
| formulas[cellId] = value; | |
| try { | |
| data[cellId] = evaluateFormula(value.substring(1)); | |
| } catch (e) { | |
| data[cellId] = '#ERROR!'; | |
| } | |
| } else { | |
| data[cellId] = value; | |
| delete formulas[cellId]; | |
| } | |
| selectedCell.textContent = data[cellId] || ''; | |
| // Save state to history | |
| saveState(); | |
| } | |
| }); | |
| // Format buttons | |
| document.getElementById('bold').addEventListener('click', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| // Initialize cell style if not exists | |
| if (!cellStyles[cellId]) { | |
| cellStyles[cellId] = {}; | |
| } | |
| // Toggle bold | |
| cellStyles[cellId].bold = !cellStyles[cellId].bold; | |
| selectedCell.style.fontWeight = cellStyles[cellId].bold ? 'bold' : 'normal'; | |
| // Toggle active class | |
| this.classList.toggle('active'); | |
| // Save state to history | |
| saveState(); | |
| } | |
| }); | |
| document.getElementById('italic').addEventListener('click', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| if (!cellStyles[cellId]) { | |
| cellStyles[cellId] = {}; | |
| } | |
| cellStyles[cellId].italic = !cellStyles[cellId].italic; | |
| selectedCell.style.fontStyle = cellStyles[cellId].italic ? 'italic' : 'normal'; | |
| this.classList.toggle('active'); | |
| saveState(); | |
| } | |
| }); | |
| document.getElementById('underline').addEventListener('click', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| if (!cellStyles[cellId]) { | |
| cellStyles[cellId] = {}; | |
| } | |
| cellStyles[cellId].underline = !cellStyles[cellId].underline; | |
| selectedCell.style.textDecoration = cellStyles[cellId].underline ? 'underline' : 'none'; | |
| this.classList.toggle('active'); | |
| saveState(); | |
| } | |
| }); | |
| // Alignment buttons | |
| document.getElementById('align-left').addEventListener('click', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| if (!cellStyles[cellId]) { | |
| cellStyles[cellId] = {}; | |
| } | |
| cellStyles[cellId].textAlign = 'left'; | |
| selectedCell.style.textAlign = 'left'; | |
| // Update active state | |
| document.querySelectorAll('.format-button').forEach(btn => btn.classList.remove('active')); | |
| this.classList.add('active'); | |
| saveState(); | |
| } | |
| }); | |
| document.getElementById('align-center').addEventListener('click', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| if (!cellStyles[cellId]) { | |
| cellStyles[cellId] = {}; | |
| } | |
| cellStyles[cellId].textAlign = 'center'; | |
| selectedCell.style.textAlign = 'center'; | |
| document.querySelectorAll('.format-button').forEach(btn => btn.classList.remove('active')); | |
| this.classList.add('active'); | |
| saveState(); | |
| } | |
| }); | |
| document.getElementById('align-right').addEventListener('click', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| if (!cellStyles[cellId]) { | |
| cellStyles[cellId] = {}; | |
| } | |
| cellStyles[cellId].textAlign = 'right'; | |
| selectedCell.style.textAlign = 'right'; | |
| document.querySelectorAll('.format-button').forEach(btn => btn.classList.remove('active')); | |
| this.classList.add('active'); | |
| saveState(); | |
| } | |
| }); | |
| // Font family change | |
| document.getElementById('font-family').addEventListener('change', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| if (!cellStyles[cellId]) { | |
| cellStyles[cellId] = {}; | |
| } | |
| cellStyles[cellId].fontFamily = this.value; | |
| selectedCell.style.fontFamily = this.value; | |
| saveState(); | |
| } | |
| }); | |
| // Font size change | |
| document.getElementById('font-size').addEventListener('change', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| if (!cellStyles[cellId]) { | |
| cellStyles[cellId] = {}; | |
| } | |
| cellStyles[cellId].fontSize = parseInt(this.value); | |
| selectedCell.style.fontSize = this.value + 'px'; | |
| saveState(); | |
| } | |
| }); | |
| // Text color button | |
| document.getElementById('text-color').addEventListener('click', function(e) { | |
| currentColorPickerType = 'text'; | |
| showColorPicker(e.currentTarget); | |
| }); | |
| // Fill color button | |
| document.getElementById('fill-color').addEventListener('click', function(e) { | |
| currentColorPickerType = 'background'; | |
| showColorPicker(e.currentTarget); | |
| }); | |
| // Remove colors button | |
| document.getElementById('remove-colors').addEventListener('click', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| if (cellStyles[cellId]) { | |
| // Remove color properties | |
| delete cellStyles[cellId].color; | |
| delete cellStyles[cellId].backgroundColor; | |
| // Apply changes | |
| applyCellStyles(selectedCell, cellId); | |
| // Save state | |
| saveState(); | |
| // Update status | |
| statusElement.textContent = "Colores eliminados"; | |
| setTimeout(() => statusElement.textContent = "Listo", 2000); | |
| } | |
| } | |
| }); | |
| // Show color picker | |
| function showColorPicker(target) { | |
| const rect = target.getBoundingClientRect(); | |
| colorPicker.style.top = `${rect.bottom + 5}px`; | |
| colorPicker.style.left = `${rect.left}px`; | |
| colorPicker.classList.remove('hidden'); | |
| } | |
| // Color picker selection | |
| colorPicker.querySelectorAll('[data-color]').forEach(color => { | |
| color.addEventListener('click', function() { | |
| const selectedColor = this.dataset.color; | |
| if (selectedCell && currentColorPickerType) { | |
| const cellId = selectedCell.dataset.id; | |
| if (!cellStyles[cellId]) { | |
| cellStyles[cellId] = {}; | |
| } | |
| if (currentColorPickerType === 'text') { | |
| cellStyles[cellId].color = selectedColor; | |
| selectedCell.style.color = selectedColor; | |
| textColorPreview.style.backgroundColor = selectedColor; | |
| } else { | |
| cellStyles[cellId].backgroundColor = selectedColor; | |
| selectedCell.style.backgroundColor = selectedColor; | |
| fillColorPreview.style.backgroundColor = selectedColor; | |
| } | |
| saveState(); | |
| } | |
| colorPicker.classList.add('hidden'); | |
| }); | |
| }); | |
| // Color picker remove | |
| document.getElementById('color-picker-remove').addEventListener('click', function() { | |
| if (selectedCell && currentColorPickerType) { | |
| const cellId = selectedCell.dataset.id; | |
| if (cellStyles[cellId]) { | |
| if (currentColorPickerType === 'text') { | |
| delete cellStyles[cellId].color; | |
| selectedCell.style.color = ''; | |
| textColorPreview.style.backgroundColor = defaultTextColor; | |
| } else { | |
| delete cellStyles[cellId].backgroundColor; | |
| selectedCell.style.backgroundColor = ''; | |
| fillColorPreview.style.backgroundColor = defaultFillColor; | |
| } | |
| saveState(); | |
| } | |
| } | |
| colorPicker.classList.add('hidden'); | |
| }); | |
| // Color picker cancel | |
| document.getElementById('color-picker-cancel').addEventListener('click', function() { | |
| colorPicker.classList.add('hidden'); | |
| }); | |
| // Clear contents button | |
| document.getElementById('clear-contents').addEventListener('click', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| // Clear data and formulas but keep formatting | |
| delete data[cellId]; | |
| delete formulas[cellId]; | |
| selectedCell.textContent = ''; | |
| // Save state | |
| saveState(); | |
| // Update status | |
| statusElement.textContent = "Contenido eliminado"; | |
| setTimeout(() => statusElement.textContent = "Listo", 2000); | |
| } | |
| }); | |
| // Clear formats button | |
| document.getElementById('clear-formats').addEventListener('click', function() { | |
| if (selectedCell) { | |
| const cellId = selectedCell.dataset.id; | |
| // Remove all formatting | |
| delete cellStyles[cellId]; | |
| // Reset cell styles | |
| selectedCell.style = ''; | |
| selectedCell.className = 'cell absolute bg-gray-800 border border-gray-700 p-1 overflow-hidden text-sm selected'; | |
| // Reset color previews | |
| textColorPreview.style.backgroundColor = defaultTextColor; | |
| fillColorPreview.style.backgroundColor = defaultFillColor; | |
| // Reset format buttons | |
| document.querySelectorAll('.format-button').forEach(btn => btn.classList.remove('active')); | |
| // Save state | |
| saveState(); | |
| // Update status | |
| statusElement.textContent = "Formato eliminado"; | |
| setTimeout(() => statusElement.textContent = "Listo", 2000); | |
| } | |
| }); | |
| // File operations | |
| document.getElementById('new-file').addEventListener('click', function() { | |
| if (confirm('¿Crear nuevo archivo? Se perderán los cambios no guardados.')) { | |
| // Reset all data | |
| data = {}; | |
| formulas = {}; | |
| cellStyles = {}; | |
| cellWidths = {}; | |
| cellHeights = {}; | |
| // Clear all cells | |
| document.querySelectorAll('.cell').forEach(cell => { | |
| cell.textContent = ''; | |
| cell.style = ''; | |
| cell.className = 'cell absolute bg-gray-800 border border-gray-700 p-1 overflow-hidden text-sm'; | |
| }); | |
| // Reset column widths | |
| for (let col = 0; col < COLS; col++) { | |
| columnHeaders.children[col].style.width = `${CELL_WIDTH}px`; | |
| document.querySelectorAll(`.cell[data-col="${col}"]`).forEach(cell => { | |
| cell.style.width = `${CELL_WIDTH}px`; | |
| }); | |
| } | |
| // Reset row heights | |
| for (let row = 0; row < ROWS; row++) { | |
| rowNumbers.children[row + 1].style.height = `${CELL_HEIGHT}px`; | |
| document.querySelectorAll(`.cell[data-row="${row}"]`).forEach(cell => { | |
| cell.style.height = `${CELL_HEIGHT}px`; | |
| }); | |
| } | |
| // Reset grid dimensions | |
| grid.style.width = `${COLS * CELL_WIDTH}px`; | |
| grid.style.height = `${ROWS * CELL_HEIGHT}px`; | |
| // Reset formula bar | |
| formulaInput.value = ''; | |
| cellReference.textContent = 'A1'; | |
| // Reset color previews | |
| textColorPreview.style.backgroundColor = defaultTextColor; | |
| fillColorPreview.style.backgroundColor = defaultFillColor; | |
| // Reset format buttons | |
| document.querySelectorAll('.format-button').forEach(btn => btn.classList.remove('active')); | |
| // Save state | |
| saveState(); | |
| // Update status | |
| statusElement.textContent = "Nuevo archivo creado"; | |
| setTimeout(() => statusElement.textContent = "Listo", 2000); | |
| } | |
| }); | |
| document.getElementById('save-file').addEventListener('click', function() { | |
| // In a real app, this would save to server or download file | |
| const excelData = { | |
| data: data, | |
| formulas: formulas, | |
| cellStyles: cellStyles, | |
| cellWidths: cellWidths, | |
| cellHeights: cellHeights | |
| }; | |
| console.log('Datos a guardar:', excelData); | |
| statusElement.textContent = "Archivo guardado (consola)"; | |
| setTimeout(() => statusElement.textContent = "Listo", 2000); | |
| }); | |
| document.getElementById('export-pdf').addEventListener('click', function() { | |
| // In a real app, this would generate a PDF | |
| statusElement.textContent = "Exportando a PDF..."; | |
| setTimeout(() => { | |
| statusElement.textContent = "PDF generado (simulado)"; | |
| setTimeout(() => statusElement.textContent = "Listo", 2000); | |
| }, 1000); | |
| }); | |
| // Edit operations | |
| document.getElementById('undo').addEventListener('click', undo); | |
| document.getElementById('redo').addEventListener('click', redo); | |
| document.getElementById('cut').addEventListener('click', cut); | |
| document.getElementById('copy').addEventListener('click', copy); | |
| document.getElementById('paste').addEventListener('click', paste); | |
| let clipboard = null; | |
| function cut() { | |
| if (selectedCell) { | |
| copy(); | |
| data[selectedCell.dataset.id] = ''; | |
| selectedCell.textContent = ''; | |
| saveState(); | |
| statusElement.textContent = "Texto cortado"; | |
| } | |
| } | |
| function copy() { | |
| if (selectedCell) { | |
| clipboard = { | |
| value: data[selectedCell.dataset.id] || '', | |
| style: cellStyles[selectedCell.dataset.id] || null | |
| }; | |
| statusElement.textContent = "Texto copiado"; | |
| } | |
| } | |
| function paste() { | |
| if (selectedCell && clipboard) { | |
| const cellId = selectedCell.dataset.id; | |
| data[cellId] = clipboard.value; | |
| if (clipboard.style) { | |
| cellStyles[cellId] = {...clipboard.style}; | |
| applyCellStyles(selectedCell, cellId); | |
| } | |
| selectedCell.textContent = data[cellId] || ''; | |
| saveState(); | |
| statusElement.textContent = "Texto pegado"; | |
| } | |
| } | |
| // Save state to history | |
| function saveState() { | |
| // Truncate history if we're not at the end | |
| if (historyIndex < history.length - 1) { | |
| history = history.slice(0, historyIndex + 1); | |
| } | |
| // Save current state | |
| history.push({ | |
| data: {...data}, | |
| formulas: {...formulas}, | |
| cellStyles: {...cellStyles}, | |
| cellWidths: {...cellWidths}, | |
| cellHeights: {...cellHeights} | |
| }); | |
| historyIndex = history.length - 1; | |
| // Limit history size | |
| if (history.length > 50) { | |
| history.shift(); | |
| historyIndex--; | |
| } | |
| } | |
| // Undo | |
| function undo() { | |
| if (historyIndex > 0) { | |
| historyIndex--; | |
| restoreState(); | |
| statusElement.textContent = "Deshacer"; | |
| } | |
| } | |
| // Redo | |
| function redo() { | |
| if (historyIndex < history.length - 1) { | |
| historyIndex++; | |
| restoreState(); | |
| statusElement.textContent = "Rehacer"; | |
| } | |
| } | |
| // Restore state from history | |
| function restoreState() { | |
| const state = history[historyIndex]; | |
| data = {...state.data}; | |
| formulas = {...state.formulas}; | |
| cellStyles = {...state.cellStyles}; | |
| cellWidths = {...state.cellWidths}; | |
| cellHeights = {...state.cellHeights}; | |
| // Update all cells | |
| document.querySelectorAll('.cell').forEach(cell => { | |
| const cellId = cell.dataset.id; | |
| cell.textContent = data[cellId] || ''; | |
| applyCellStyles(cell, cellId); | |
| }); | |
| // Update column widths | |
| for (let col = 0; col < COLS; col++) { | |
| const colLetter = String.fromCharCode(65 + col); | |
| const width = cellWidths[colLetter] || CELL_WIDTH; | |
| columnHeaders.children[col].style.width = `${width}px`; | |
| const cells = document.querySelectorAll(`.cell[data-col="${col}"]`); | |
| cells.forEach(cell => { | |
| cell.style.width = `${width}px`; | |
| }); | |
| } | |
| // Update row heights | |
| for (let row = 0; row < ROWS; row++) { | |
| const height = cellHeights[row + 1] || CELL_HEIGHT; | |
| rowNumbers.children[row + 1].style.height = `${height}px`; | |
| const cells = document.querySelectorAll(`.cell[data-row="${row}"]`); | |
| cells.forEach(cell => { | |
| cell.style.height = `${height}px`; | |
| }); | |
| } | |
| // Update grid dimensions | |
| updateGridDimensions(); | |
| // Update color previews | |
| if (selectedCell) { | |
| updateColorPreviews(selectedCell.dataset.id); | |
| updateFormatButtons(selectedCell.dataset.id); | |
| } | |
| } | |
| // Insert function button | |
| document.getElementById('insert-function').addEventListener('click', function() { | |
| if (selectedCell) { | |
| formulaInput.value = '=SUM('; | |
| formulaInput.focus(); | |
| statusElement.textContent = "Insertando función SUM"; | |
| } | |
| }); | |
| // Sort buttons | |
| document.getElementById('sort-asc').addEventListener('click', function() { | |
| statusElement.textContent = "Orden ascendente (simulado)"; | |
| }); | |
| document.getElementById('sort-desc').addEventListener('click', function() { | |
| statusElement.textContent = "Orden descendente (simulado)"; | |
| }); | |
| // Filter button | |
| document.getElementById('filter').addEventListener('click', function() { | |
| statusElement.textContent = "Filtro aplicado (simulado)"; | |
| }); | |
| // Chart button | |
| document.getElementById('chart').addEventListener('click', function() { | |
| statusElement.textContent = "Creando gráfico (simulado)"; | |
| setTimeout(() => { | |
| alert('Gráfico creado (simulación)'); | |
| statusElement.textContent = "Listo"; | |
| }, 1000); | |
| }); | |
| // Zoom buttons | |
| document.getElementById('zoom-in').addEventListener('click', function() { | |
| const currentZoom = parseInt(gridContainer.style.zoom || '100'); | |
| const newZoom = Math.min(currentZoom + 10, 200); | |
| gridContainer.style.zoom = `${newZoom}%`; | |
| document.querySelector('.text-pink-400').textContent = `Zoom: ${newZoom}%`; | |
| statusElement.textContent = `Zoom: ${newZoom}%`; | |
| }); | |
| document.getElementById('zoom-out').addEventListener('click', function() { | |
| const currentZoom = parseInt(gridContainer.style.zoom || '100'); | |
| const newZoom = Math.max(currentZoom - 10, 50); | |
| gridContainer.style.zoom = `${newZoom}%`; | |
| document.querySelector('.text-pink-400').textContent = `Zoom: ${newZoom}%`; | |
| statusElement.textContent = `Zoom: ${newZoom}%`; | |
| }); | |
| document.getElementById('zoom-reset').addEventListener('click', function() { | |
| gridContainer.style.zoom = '100%'; | |
| document.querySelector('.text-pink-400').textContent = 'Zoom: 100%'; | |
| statusElement.textContent = 'Zoom: 100%'; | |
| }); | |
| // Add sheet button | |
| document.getElementById('add-sheet').addEventListener('click', function() { | |
| const sheetButtons = document.querySelectorAll('.tab-active + button, .tab-active'); | |
| const lastSheet = sheetButtons[sheetButtons.length - 1]; | |
| const sheetNumber = parseInt(lastSheet.textContent.replace('Hoja', '')) + 1; | |
| const newSheetButton = document.createElement('button'); | |
| newSheetButton.className = 'px-3 py-1 hover:bg-gray-700 text-sm font-medium text-gray-400'; | |
| newSheetButton.textContent = `Hoja${sheetNumber}`; | |
| lastSheet.parentNode.insertBefore(newSheetButton, lastSheet.nextSibling); | |
| // Add to sheets data | |
| sheets[`Hoja${sheetNumber}`] = { data: {}, formulas: {}, cellStyles: {} }; | |
| statusElement.textContent = `Hoja${sheetNumber} añadida`; | |
| }); | |
| // Sheet tab click | |
| document.querySelectorAll('[class*="tab-"]').forEach(tab => { | |
| tab.addEventListener('click', function() { | |
| if (!this.classList.contains('tab-active')) { | |
| // Switch sheet | |
| document.querySelector('.tab-active').classList.remove('tab-active', 'text-blue-400'); | |
| document.querySelector('.tab-active').classList.add('text-gray-400'); | |
| this.classList.add('tab-active', 'text-blue-400'); | |
| this.classList.remove('text-gray-400'); | |
| // Update active sheet | |
| activeSheet = this.textContent.trim(); | |
| // In a real app, we would load the sheet data | |
| statusElement.textContent = `Cambiado a ${activeSheet}`; | |
| } | |
| }); | |
| }); | |
| // Detect num lock and caps lock | |
| document.addEventListener('keydown', function(e) { | |
| if (e.getModifierState('NumLock')) { | |
| numLockElement.classList.remove('hidden'); | |
| } else { | |
| numLockElement.classList.add('hidden'); | |
| } | |
| if (e.getModifierState('CapsLock')) { | |
| capsLockElement.classList.remove('hidden'); | |
| } else { | |
| capsLockElement.classList.add('hidden'); | |
| } | |
| }); | |
| // Initialize | |
| initGrid(); | |
| // Add keyboard navigation | |
| document.addEventListener('keydown', function(e) { | |
| if (!selectedCell) return; | |
| const row = parseInt(selectedCell.dataset.row); | |
| const col = parseInt(selectedCell.dataset.col); | |
| if (e.key === 'ArrowRight' && col < COLS - 1) { | |
| const nextCell = document.querySelector(`.cell[data-row="${row}"][data-col="${col + 1}"]`); | |
| nextCell.click(); | |
| } else if (e.key === 'ArrowLeft' && col > 0) { | |
| const nextCell = document.querySelector(`.cell[data-row="${row}"][data-col="${col - 1}"]`); | |
| nextCell.click(); | |
| } else if (e.key === 'ArrowDown' && row < ROWS - 1) { | |
| const nextCell = document.querySelector(`.cell[data-row="${row + 1}"][data-col="${col}"]`); | |
| nextCell.click(); | |
| } else if (e.key === 'ArrowUp' && row > 0) { | |
| const nextCell = document.querySelector(`.cell[data-row="${row - 1}"][data-col="${col}"]`); | |
| nextCell.click(); | |
| } else if (e.key === 'Enter') { | |
| selectedCell.dispatchEvent(new MouseEvent('dblclick')); | |
| } else if (e.key === 'F2') { | |
| selectedCell.dispatchEvent(new MouseEvent('dblclick')); | |
| } else if (e.key === 'Tab') { | |
| e.preventDefault(); | |
| if (e.shiftKey && col > 0) { | |
| const prevCell = document.querySelector(`.cell[data-row="${row}"][data-col="${col - 1}"]`); | |
| prevCell.click(); | |
| } else if (col < COLS - 1) { | |
| const nextCell = document.querySelector(`.cell[data-row="${row}"][data-col="${col + 1}"]`); | |
| nextCell.click(); | |
| } | |
| } | |
| }); | |
| }); | |
| </script> | |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=LJMolotov/my-multi-notes" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> | |
| </html> |