BuiMinh commited on
Commit
0157e98
·
verified ·
1 Parent(s): 48329ff

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +468 -19
index.html CHANGED
@@ -1,19 +1,468 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>SpreadMaster - Excel Clone</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <script src="https://unpkg.com/feather-icons"></script>
9
+ <script>
10
+ tailwind.config = {
11
+ theme: {
12
+ extend: {
13
+ colors: {
14
+ primary: { DEFAULT: '#4F46E5', 50: '#F5F5FF', 500: '#4F46E5' },
15
+ secondary: { DEFAULT: '#10B981', 50: '#ECFDF5', 500: '#10B981' }
16
+ }
17
+ }
18
+ }
19
+ }
20
+ </script>
21
+ <style>
22
+ .cell { position: relative; outline: none; }
23
+ .cell:focus, .cell.selected { @apply ring-2 ring-primary-500 z-10; }
24
+ .grid-container { display: grid; position: relative; scrollbar-width: thin; }
25
+ .cell[data-formula="true"] { @apply font-mono bg-primary-50 text-primary-700; }
26
+ .header-row { position: sticky; top: 0; z-index: 20; background: white; }
27
+ .header-col { position: sticky; left: 0; z-index: 15; background: white; }
28
+ .corner-header { position: sticky; top: 0; left: 0; z-index: 30; background: #f3f4f6; }
29
+ .cell-resize-handle {
30
+ position: absolute; bottom: 0; right: 0; width: 6px; height: 6px;
31
+ background: #4F46E5; border: 1px solid white; cursor: se-resize; z-index: 5;
32
+ display: none;
33
+ }
34
+ .cell.selected .cell-resize-handle { display: block; }
35
+ .cell-content {
36
+ min-height: 100%; width: 100%; padding: 4px; outline: none;
37
+ background: transparent; resize: none; overflow: hidden;
38
+ }
39
+ .cell-content:focus { @apply bg-white; }
40
+ .header-row, .header-col, .corner-header {
41
+ @apply flex items-center justify-center font-medium text-gray-600 select-none border border-gray-300;
42
+ }
43
+ .formula-bar { background: white; border: 1px solid #e5e7eb; }
44
+ .function-list {
45
+ @apply absolute top-full left-0 bg-white border border-gray-300 rounded-lg shadow-lg mt-1 z-50 w-48 max-h-64 overflow-auto;
46
+ }
47
+ .function-item { @apply px-3 py-2 hover:bg-gray-100 cursor-pointer text-sm; }
48
+ .function-item:hover { @apply bg-primary-50 text-primary-700; }
49
+ </style>
50
+ </head>
51
+ <body class="bg-gray-50 min-h-screen">
52
+ <div class="container mx-auto px-4 py-8 max-w-7xl">
53
+ <!-- Header -->
54
+ <div class="flex items-center justify-between mb-6">
55
+ <div class="flex space-x-2">
56
+ <button onclick="saveData()" class="flex items-center space-x-2 px-4 py-2 bg-gray-200 hover:bg-gray-300 text-gray-700 rounded-lg">
57
+ <i data-feather="save"></i><span>Save</span>
58
+ </button>
59
+ <button onclick="addColumn()" class="flex items-center space-x-2 px-4 py-2 bg-primary-500 hover:bg-primary-600 text-white rounded-lg">
60
+ <i data-feather="plus"></i><span>Add Column(Cột)</span>
61
+ </button>
62
+ <button onclick="addRow()" class="flex items-center space-x-2 px-4 py-2 bg-secondary-500 hover:bg-secondary-600 text-white rounded-lg">
63
+ <i data-feather="plus"></i><span>Add Row(Hàng)</span>
64
+ </button>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- Formula Bar -->
69
+ <div class="mb-4 bg-white rounded-lg shadow-sm border border-gray-200">
70
+ <div class="flex items-center p-2 border-b border-gray-200">
71
+ <div class="flex items-center space-x-2 flex-1">
72
+ <span class="text-xs font-medium text-gray-500">fx</span>
73
+ <input type="text" id="cell-name" class="w-20 px-2 py-1 bg-gray-100 text-xs font-mono text-right border rounded" readonly>
74
+ <button id="function-btn" class="px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded" onclick="toggleFunctionList()">
75
+ <i data-feather="chevron-down" class="w-3 h-3"></i>
76
+ </button>
77
+ </div>
78
+ <input type="text" id="formula-bar" class="formula-bar flex-1 px-4 py-2 outline-none font-mono mx-2"
79
+ placeholder="Enter formula or value" onkeydown="handleFormulaInput(event)"
80
+ onfocus="syncFormulaBarWithCell()">
81
+ </div>
82
+ <div id="function-list" class="function-list hidden">
83
+ <div class="px-3 py-2 border-b border-gray-200 font-medium text-sm">Common Functions</div>
84
+ <div class="function-item" onclick="insertFunction('SUM')">SUM(range)</div>
85
+ <div class="function-item" onclick="insertFunction('AVERAGE')">AVERAGE(range)</div>
86
+ <div class="function-item" onclick="insertFunction('COUNT')">COUNT(range)</div>
87
+ <div class="function-item" onclick="insertFunction('MAX')">MAX(range)</div>
88
+ <div class="function-item" onclick="insertFunction('MIN')">MIN(range)</div>
89
+ <div class="function-item" onclick="insertFunction('IF')">IF(condition, true, false)</div>
90
+ </div>
91
+ </div>
92
+
93
+ <!-- Grid -->
94
+ <div class="relative">
95
+ <div id="grid" class="grid-container bg-gray-200 border border-gray-200 rounded-lg overflow-auto shadow-sm max-h-[600px]"></div>
96
+ </div>
97
+
98
+ <!-- Status -->
99
+ <div class="mt-4 px-4 py-2 bg-white rounded-lg shadow-sm text-sm text-gray-500 flex justify-between">
100
+ <div id="status-message">Ready</div>
101
+ </div>
102
+ </div>
103
+
104
+ <script>
105
+ let numRows = 20, numCols = 10;
106
+ const colLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
107
+ let selectedCell = null, data = {}, isDragging = false, startCell = null, dragEndCell = null;
108
+
109
+ const functions = {
110
+ SUM: (range) => getRangeValues(range).reduce((a, b) => a + (parseFloat(b) || 0), 0),
111
+ AVERAGE: (range) => {
112
+ const values = getRangeValues(range).filter(v => v !== '' && !isNaN(parseFloat(v)));
113
+ return values.length ? functions.SUM(range) / values.length : 0;
114
+ },
115
+ COUNT: (range) => getRangeValues(range).filter(v => v !== '' && !isNaN(parseFloat(v))).length,
116
+ MAX: (range) => Math.max(...getRangeValues(range).map(v => parseFloat(v) || -Infinity)),
117
+ MIN: (range) => Math.min(...getRangeValues(range).map(v => parseFloat(v) || Infinity)),
118
+ IF: (condition, trueVal, falseVal) => evalCondition(condition) ? trueVal : falseVal
119
+ };
120
+
121
+ document.addEventListener('DOMContentLoaded', () => {
122
+ loadData();
123
+ initGrid();
124
+ feather.replace();
125
+ });
126
+
127
+ function initGrid() {
128
+ const grid = document.getElementById('grid');
129
+ grid.innerHTML = '';
130
+ grid.style.gridTemplateColumns = `50px repeat(${numCols}, minmax(120px, 1fr))`;
131
+ grid.style.gridTemplateRows = `40px repeat(${numRows}, 40px)`;
132
+
133
+ // Corner
134
+ const corner = document.createElement('div');
135
+ corner.className = 'corner-header bg-gray-100';
136
+ corner.style.gridArea = '1 / 1';
137
+ grid.appendChild(corner);
138
+
139
+ // Column headers
140
+ for (let col = 0; col < numCols; col++) {
141
+ const header = document.createElement('div');
142
+ header.className = 'header-row bg-gray-100';
143
+ header.textContent = colLetters[col];
144
+ header.style.gridArea = '1 / ' + (col + 2);
145
+ header.onclick = () => selectColumn(col);
146
+ grid.appendChild(header);
147
+ }
148
+
149
+ // Rows and cells
150
+ for (let row = 1; row <= numRows; row++) {
151
+ // Row header
152
+ const rowHeader = document.createElement('div');
153
+ rowHeader.className = 'header-col bg-gray-100';
154
+ rowHeader.textContent = row;
155
+ rowHeader.style.gridArea = (row + 1) + ' / 1';
156
+ rowHeader.onclick = () => selectRow(row);
157
+ grid.appendChild(rowHeader);
158
+
159
+ // Cells
160
+ for (let col = 0; col < numCols; col++) {
161
+ const cellId = getCellId(col, row);
162
+ const cell = createCell(col, row, cellId);
163
+ grid.appendChild(cell);
164
+ }
165
+ }
166
+ }
167
+
168
+ function createCell(col, row, cellId) {
169
+ const cell = document.createElement('div');
170
+ cell.className = 'cell bg-white border border-gray-200 p-0 hover:bg-gray-50 cursor-cell';
171
+ cell.dataset.col = col; cell.dataset.row = row; cell.tabIndex = 0; cell.id = cellId;
172
+
173
+ const handle = document.createElement('div');
174
+ handle.className = 'cell-resize-handle';
175
+ handle.onmousedown = (e) => startAutoFill(e, col, row);
176
+ cell.appendChild(handle);
177
+
178
+ const content = document.createElement('div');
179
+ content.className = 'cell-content'; content.contentEditable = true;
180
+ content.oninput = () => updateCellValue(cell, col, row);
181
+ content.onfocus = () => selectCell(cell, col, row);
182
+ content.onkeydown = handleCellKeydown;
183
+
184
+ // Load data
185
+ if (data[cellId]) {
186
+ content.textContent = data[cellId].displayValue || data[cellId].value;
187
+ if (data[cellId].formula) cell.dataset.formula = 'true';
188
+ }
189
+
190
+ cell.appendChild(content);
191
+ cell.style.gridArea = (row + 1) + ' / ' + (col + 2);
192
+ cell.onclick = (e) => { e.stopPropagation(); selectCell(cell, col, row); content.focus(); };
193
+ return cell;
194
+ }
195
+
196
+ function getCellId(col, row) { return colLetters[col] + row; }
197
+ function getCellValue(cellId) { return data[cellId]?.value || '0'; }
198
+
199
+ function selectCell(cell, col, row) {
200
+ if (selectedCell) selectedCell.classList.remove('selected');
201
+ selectedCell = cell; cell.classList.add('selected');
202
+ document.getElementById('cell-name').value = getCellId(col, row);
203
+ syncFormulaBarWithCell();
204
+ document.getElementById('status-message').textContent = `Editing ${getCellId(col, row)}`;
205
+ }
206
+
207
+ function syncFormulaBarWithCell() {
208
+ if (!selectedCell) return;
209
+ const col = parseInt(selectedCell.dataset.col), row = parseInt(selectedCell.dataset.row);
210
+ const cellId = getCellId(col, row), formulaBar = document.getElementById('formula-bar');
211
+ formulaBar.value = data[cellId]?.formula ? '=' + data[cellId].formula : (data[cellId]?.value || '');
212
+ }
213
+
214
+ function handleCellKeydown(e) {
215
+ const cell = e.target.parentElement, col = parseInt(cell.dataset.col), row = parseInt(cell.dataset.row);
216
+ if (e.key === 'F2') { e.preventDefault(); document.getElementById('formula-bar').focus(); return; }
217
+ if (['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp'].includes(e.key)) {
218
+ e.preventDefault();
219
+ let nextCol = col, nextRow = row;
220
+ if (e.key === 'ArrowRight' && col < numCols - 1) nextCol++;
221
+ else if (e.key === 'ArrowLeft' && col > 0) nextCol--;
222
+ else if (e.key === 'ArrowDown' && row < numRows) nextRow++;
223
+ else if (e.key === 'ArrowUp' && row > 1) nextRow--;
224
+ const nextCell = document.getElementById(getCellId(nextCol, nextRow));
225
+ if (nextCell) nextCell.querySelector('.cell-content').focus();
226
+ }
227
+ }
228
+
229
+ function updateCellValue(cell, col, row) {
230
+ const content = cell.querySelector('.cell-content'), cellId = getCellId(col, row);
231
+ const value = content.textContent;
232
+ if (!data[cellId]) data[cellId] = { value: '', formula: '', displayValue: '' };
233
+ data[cellId].value = value; data[cellId].displayValue = value;
234
+ }
235
+
236
+ function handleFormulaInput(e) {
237
+ if (e.key !== 'Enter' || !selectedCell) return;
238
+ const formulaBar = e.target, input = formulaBar.value.trim();
239
+ const content = selectedCell.querySelector('.cell-content');
240
+ const col = parseInt(selectedCell.dataset.col), row = parseInt(selectedCell.dataset.row);
241
+ const cellId = getCellId(col, row);
242
+
243
+ if (input.toLowerCase().startsWith('del')) {
244
+ handleDelete(input); formulaBar.value = ''; return;
245
+ }
246
+
247
+ let formula = '', result = '';
248
+ if (input.startsWith('=')) {
249
+ formula = input.slice(1);
250
+ result = evaluateFormula(formula, cellId);
251
+ content.textContent = result;
252
+ selectedCell.dataset.formula = 'true';
253
+ data[cellId] = { value: result, formula, displayValue: result };
254
+ recalculateDependents(cellId);
255
+ } else {
256
+ content.textContent = input;
257
+ if (selectedCell.dataset.formula) delete selectedCell.dataset.formula;
258
+ data[cellId] = { value: input, formula: '', displayValue: input };
259
+ }
260
+ formulaBar.value = ''; content.focus();
261
+ }
262
+
263
+ function evaluateFormula(formula, currentCellId) {
264
+ try {
265
+ // Replace cell references
266
+ formula = formula.replace(/[A-Z]\d+/g, (match) => {
267
+ const col = colLetters.indexOf(match[0]), row = parseInt(match.slice(1));
268
+ const value = getCellValue(getCellId(col, row));
269
+ return isNaN(value) ? `"${value}"` : value;
270
+ });
271
+
272
+ // Parse function
273
+ const funcMatch = formula.match(/^([A-Z]+)\((.+)\)$/i);
274
+ if (funcMatch) {
275
+ const funcName = funcMatch[1].toUpperCase(), argsStr = funcMatch[2];
276
+ const args = argsStr.split(',').map(arg => arg.trim().replace(/"/g, ''));
277
+ if (functions[funcName]) {
278
+ const firstArg = args[0];
279
+ if (firstArg.includes(':')) {
280
+ return functions[funcName](firstArg);
281
+ } else {
282
+ return functions[funcName](...args.map(arg => isNaN(arg) ? arg : parseFloat(arg)));
283
+ }
284
+ }
285
+ }
286
+
287
+ // Simple math
288
+ return eval(formula);
289
+ } catch {
290
+ return '#ERROR!';
291
+ }
292
+ }
293
+
294
+ function getRangeValues(range) {
295
+ const [start, end] = range.split(':');
296
+ const startCol = colLetters.indexOf(start[0]), startRow = parseInt(start.slice(1));
297
+ const endCol = colLetters.indexOf(end[0]), endRow = parseInt(end.slice(1));
298
+ const values = [];
299
+ for (let c = Math.min(startCol, endCol); c <= Math.max(startCol, endCol); c++) {
300
+ for (let r = Math.min(startRow, endRow); r <= Math.max(startRow, endRow); r++) {
301
+ values.push(getCellValue(getCellId(c, r)));
302
+ }
303
+ }
304
+ return values;
305
+ }
306
+
307
+ function evalCondition(condition) {
308
+ try { return eval(condition); } catch { return false; }
309
+ }
310
+
311
+ function recalculateDependents(cellId) {
312
+ Object.keys(data).forEach(id => {
313
+ if (data[id].formula && data[id].formula.includes(cellId)) {
314
+ const result = evaluateFormula(data[id].formula, id);
315
+ const cell = document.getElementById(id);
316
+ if (cell) {
317
+ cell.querySelector('.cell-content').textContent = result;
318
+ data[id].value = result; data[id].displayValue = result;
319
+ }
320
+ }
321
+ });
322
+ }
323
+
324
+ // Auto-fill
325
+ function startAutoFill(e, col, row) {
326
+ e.preventDefault(); e.stopPropagation();
327
+ isDragging = true; startCell = { col, row };
328
+ document.body.style.cursor = 'se-resize';
329
+ document.addEventListener('mousemove', handleAutoFillDrag);
330
+ document.addEventListener('mouseup', endAutoFill);
331
+ }
332
+
333
+ function handleAutoFillDrag(e) {
334
+ if (!isDragging) return;
335
+ const grid = document.getElementById('grid'), rect = grid.getBoundingClientRect();
336
+ const x = e.clientX - rect.left, y = e.clientY - rect.top;
337
+ let endCol = Math.floor((x - 50) / 120), endRow = Math.floor((y - 40) / 40) + 1;
338
+ endCol = Math.max(0, Math.min(endCol, numCols - 1));
339
+ endRow = Math.max(1, Math.min(endRow, numRows));
340
+ dragEndCell = { col: endCol, row: endRow };
341
+ highlightRange(startCell.col, startCell.row, endCol, endRow);
342
+ }
343
+
344
+ function endAutoFill() {
345
+ if (!isDragging || !startCell || !dragEndCell) return;
346
+ isDragging = false; document.body.style.cursor = 'default';
347
+ performAutoFill(startCell.col, startCell.row, dragEndCell.col, dragEndCell.row);
348
+ clearHighlights();
349
+ document.removeEventListener('mousemove', handleAutoFillDrag);
350
+ document.removeEventListener('mouseup', endAutoFill);
351
+ startCell = dragEndCell = null;
352
+ }
353
+
354
+ function highlightRange(sCol, sRow, eCol, eRow) {
355
+ clearHighlights();
356
+ const minC = Math.min(sCol, eCol), maxC = Math.max(sCol, eCol);
357
+ const minR = Math.min(sRow, eRow), maxR = Math.max(sRow, eRow);
358
+ for (let c = minC; c <= maxC; c++) for (let r = minR; r <= maxR; r++) {
359
+ const cell = document.getElementById(getCellId(c, r));
360
+ if (cell) cell.classList.add('bg-blue-100');
361
+ }
362
+ }
363
+
364
+ function clearHighlights() {
365
+ document.querySelectorAll('.cell').forEach(cell => cell.classList.remove('bg-blue-100'));
366
+ }
367
+
368
+ function performAutoFill(sCol, sRow, eCol, eRow) {
369
+ const startId = getCellId(sCol, sRow);
370
+ const startData = data[startId] || { value: '', formula: '' };
371
+ const isFormula = !!startData.formula;
372
+ const deltaCol = eCol - sCol, deltaRow = eRow - sRow;
373
+ const dirCol = deltaCol !== 0 ? (deltaCol > 0 ? 1 : -1) : 0;
374
+ const dirRow = deltaRow !== 0 ? (deltaRow > 0 ? 1 : -1) : 0;
375
+
376
+ let step = 1;
377
+ let currentCol = sCol + dirCol, currentRow = sRow + dirRow;
378
+ while (dirCol ? (dirCol > 0 ? currentCol <= eCol : currentCol >= eCol) : true &&
379
+ dirRow ? (dirRow > 0 ? currentRow <= eRow : currentRow >= eRow) : true) {
380
+ const cellId = getCellId(currentCol, currentRow);
381
+ const cell = document.getElementById(cellId);
382
+ if (cell) {
383
+ let newValue, newFormula = '';
384
+ if (isFormula) {
385
+ newFormula = adjustFormulaReferences(startData.formula, step * dirRow, step * dirCol);
386
+ newValue = evaluateFormula(newFormula, cellId);
387
+ } else {
388
+ const startValue = parseFloat(startData.value);
389
+ if (!isNaN(startValue)) {
390
+ newValue = startValue + step * (dirRow || dirCol);
391
+ } else {
392
+ newValue = startData.value;
393
+ }
394
+ }
395
+ cell.querySelector('.cell-content').textContent = newValue;
396
+ data[cellId] = { value: newValue, formula: newFormula, displayValue: newValue };
397
+ if (newFormula) cell.dataset.formula = 'true';
398
+ }
399
+ currentCol += dirCol; currentRow += dirRow; step++;
400
+ }
401
+ }
402
+
403
+ function adjustFormulaReferences(formula, deltaRow, deltaCol) {
404
+ return formula.replace(/([A-Z])(\d+)/g, (match, colLetter, rowStr) => {
405
+ const col = colLetters.indexOf(colLetter);
406
+ const row = parseInt(rowStr);
407
+ const newCol = col + deltaCol;
408
+ const newRow = row + deltaRow;
409
+ return (newCol >= 0 ? colLetters[newCol] : colLetter) + newRow;
410
+ });
411
+ }
412
+
413
+ // Other functions
414
+ function toggleFunctionList() { document.getElementById('function-list').classList.toggle('hidden'); }
415
+ function insertFunction(name) {
416
+ document.getElementById('formula-bar').value += `=${name}(`;
417
+ document.getElementById('formula-bar').focus();
418
+ toggleFunctionList();
419
+ }
420
+
421
+ function handleDelete(input) {
422
+ const parts = input.toLowerCase().split(/\s+/);
423
+ if (parts.length === 1) {
424
+ selectedCell.querySelector('.cell-content').textContent = '';
425
+ delete data[selectedCell.id]; delete selectedCell.dataset.formula;
426
+ } else if (parts.length === 2) {
427
+ const target = parts[1].toUpperCase();
428
+ if (colLetters.includes(target[0])) deleteColumn(colLetters.indexOf(target[0]));
429
+ else if (!isNaN(target)) deleteRow(parseInt(target));
430
+ }
431
+ }
432
+
433
+ function deleteColumn(col) {
434
+ for (let r = 1; r <= numRows; r++) delete data[getCellId(col, r)];
435
+ for (let c = col; c < numCols - 1; c++) for (let r = 1; r <= numRows; r++) {
436
+ const oldId = getCellId(c + 1, r), newId = getCellId(c, r);
437
+ if (data[oldId]) { data[newId] = data[oldId]; delete data[oldId]; }
438
+ }
439
+ numCols--; initGrid();
440
+ }
441
+
442
+ function deleteRow(row) {
443
+ for (let c = 0; c < numCols; c++) delete data[getCellId(c, row)];
444
+ for (let r = row; r < numRows; r++) for (let c = 0; c < numCols; c++) {
445
+ const oldId = getCellId(c, r + 1), newId = getCellId(c, r);
446
+ if (data[oldId]) { data[newId] = data[oldId]; delete data[oldId]; }
447
+ }
448
+ numRows--; initGrid();
449
+ }
450
+
451
+ function addColumn() { numCols++; initGrid(); }
452
+ function addRow() { numRows++; initGrid(); }
453
+ function saveData() { localStorage.setItem('spreadmaster', JSON.stringify({data, numRows, numCols})); }
454
+ function loadData() {
455
+ const saved = localStorage.getItem('spreadmaster');
456
+ if (saved) {
457
+ const {data: d, numRows: r, numCols: c} = JSON.parse(saved);
458
+ data = d || {}; numRows = r || 20; numCols = c || 10;
459
+ }
460
+ }
461
+
462
+ // Selection helpers
463
+ function selectColumn(col) { document.getElementById('status-message').textContent = `Column ${colLetters[col]}`; }
464
+ function selectRow(row) { document.getElementById('status-message').textContent = `Row ${row}`; }
465
+ </script>
466
+ <script>feather.replace();</script>
467
+ </body>
468
+ </html>