sh / index.html
BuiMinh's picture
Update index.html
0157e98 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SpreadMaster - Excel Clone</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/feather-icons"></script>
<script>
tailwind.config = {
theme: {
extend: {
colors: {
primary: { DEFAULT: '#4F46E5', 50: '#F5F5FF', 500: '#4F46E5' },
secondary: { DEFAULT: '#10B981', 50: '#ECFDF5', 500: '#10B981' }
}
}
}
}
</script>
<style>
.cell { position: relative; outline: none; }
.cell:focus, .cell.selected { @apply ring-2 ring-primary-500 z-10; }
.grid-container { display: grid; position: relative; scrollbar-width: thin; }
.cell[data-formula="true"] { @apply font-mono bg-primary-50 text-primary-700; }
.header-row { position: sticky; top: 0; z-index: 20; background: white; }
.header-col { position: sticky; left: 0; z-index: 15; background: white; }
.corner-header { position: sticky; top: 0; left: 0; z-index: 30; background: #f3f4f6; }
.cell-resize-handle {
position: absolute; bottom: 0; right: 0; width: 6px; height: 6px;
background: #4F46E5; border: 1px solid white; cursor: se-resize; z-index: 5;
display: none;
}
.cell.selected .cell-resize-handle { display: block; }
.cell-content {
min-height: 100%; width: 100%; padding: 4px; outline: none;
background: transparent; resize: none; overflow: hidden;
}
.cell-content:focus { @apply bg-white; }
.header-row, .header-col, .corner-header {
@apply flex items-center justify-center font-medium text-gray-600 select-none border border-gray-300;
}
.formula-bar { background: white; border: 1px solid #e5e7eb; }
.function-list {
@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;
}
.function-item { @apply px-3 py-2 hover:bg-gray-100 cursor-pointer text-sm; }
.function-item:hover { @apply bg-primary-50 text-primary-700; }
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8 max-w-7xl">
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div class="flex space-x-2">
<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">
<i data-feather="save"></i><span>Save</span>
</button>
<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">
<i data-feather="plus"></i><span>Add Column(Cột)</span>
</button>
<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">
<i data-feather="plus"></i><span>Add Row(Hàng)</span>
</button>
</div>
</div>
<!-- Formula Bar -->
<div class="mb-4 bg-white rounded-lg shadow-sm border border-gray-200">
<div class="flex items-center p-2 border-b border-gray-200">
<div class="flex items-center space-x-2 flex-1">
<span class="text-xs font-medium text-gray-500">fx</span>
<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>
<button id="function-btn" class="px-2 py-1 text-xs bg-gray-100 hover:bg-gray-200 rounded" onclick="toggleFunctionList()">
<i data-feather="chevron-down" class="w-3 h-3"></i>
</button>
</div>
<input type="text" id="formula-bar" class="formula-bar flex-1 px-4 py-2 outline-none font-mono mx-2"
placeholder="Enter formula or value" onkeydown="handleFormulaInput(event)"
onfocus="syncFormulaBarWithCell()">
</div>
<div id="function-list" class="function-list hidden">
<div class="px-3 py-2 border-b border-gray-200 font-medium text-sm">Common Functions</div>
<div class="function-item" onclick="insertFunction('SUM')">SUM(range)</div>
<div class="function-item" onclick="insertFunction('AVERAGE')">AVERAGE(range)</div>
<div class="function-item" onclick="insertFunction('COUNT')">COUNT(range)</div>
<div class="function-item" onclick="insertFunction('MAX')">MAX(range)</div>
<div class="function-item" onclick="insertFunction('MIN')">MIN(range)</div>
<div class="function-item" onclick="insertFunction('IF')">IF(condition, true, false)</div>
</div>
</div>
<!-- Grid -->
<div class="relative">
<div id="grid" class="grid-container bg-gray-200 border border-gray-200 rounded-lg overflow-auto shadow-sm max-h-[600px]"></div>
</div>
<!-- Status -->
<div class="mt-4 px-4 py-2 bg-white rounded-lg shadow-sm text-sm text-gray-500 flex justify-between">
<div id="status-message">Ready</div>
</div>
</div>
<script>
let numRows = 20, numCols = 10;
const colLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
let selectedCell = null, data = {}, isDragging = false, startCell = null, dragEndCell = null;
const functions = {
SUM: (range) => getRangeValues(range).reduce((a, b) => a + (parseFloat(b) || 0), 0),
AVERAGE: (range) => {
const values = getRangeValues(range).filter(v => v !== '' && !isNaN(parseFloat(v)));
return values.length ? functions.SUM(range) / values.length : 0;
},
COUNT: (range) => getRangeValues(range).filter(v => v !== '' && !isNaN(parseFloat(v))).length,
MAX: (range) => Math.max(...getRangeValues(range).map(v => parseFloat(v) || -Infinity)),
MIN: (range) => Math.min(...getRangeValues(range).map(v => parseFloat(v) || Infinity)),
IF: (condition, trueVal, falseVal) => evalCondition(condition) ? trueVal : falseVal
};
document.addEventListener('DOMContentLoaded', () => {
loadData();
initGrid();
feather.replace();
});
function initGrid() {
const grid = document.getElementById('grid');
grid.innerHTML = '';
grid.style.gridTemplateColumns = `50px repeat(${numCols}, minmax(120px, 1fr))`;
grid.style.gridTemplateRows = `40px repeat(${numRows}, 40px)`;
// Corner
const corner = document.createElement('div');
corner.className = 'corner-header bg-gray-100';
corner.style.gridArea = '1 / 1';
grid.appendChild(corner);
// Column headers
for (let col = 0; col < numCols; col++) {
const header = document.createElement('div');
header.className = 'header-row bg-gray-100';
header.textContent = colLetters[col];
header.style.gridArea = '1 / ' + (col + 2);
header.onclick = () => selectColumn(col);
grid.appendChild(header);
}
// Rows and cells
for (let row = 1; row <= numRows; row++) {
// Row header
const rowHeader = document.createElement('div');
rowHeader.className = 'header-col bg-gray-100';
rowHeader.textContent = row;
rowHeader.style.gridArea = (row + 1) + ' / 1';
rowHeader.onclick = () => selectRow(row);
grid.appendChild(rowHeader);
// Cells
for (let col = 0; col < numCols; col++) {
const cellId = getCellId(col, row);
const cell = createCell(col, row, cellId);
grid.appendChild(cell);
}
}
}
function createCell(col, row, cellId) {
const cell = document.createElement('div');
cell.className = 'cell bg-white border border-gray-200 p-0 hover:bg-gray-50 cursor-cell';
cell.dataset.col = col; cell.dataset.row = row; cell.tabIndex = 0; cell.id = cellId;
const handle = document.createElement('div');
handle.className = 'cell-resize-handle';
handle.onmousedown = (e) => startAutoFill(e, col, row);
cell.appendChild(handle);
const content = document.createElement('div');
content.className = 'cell-content'; content.contentEditable = true;
content.oninput = () => updateCellValue(cell, col, row);
content.onfocus = () => selectCell(cell, col, row);
content.onkeydown = handleCellKeydown;
// Load data
if (data[cellId]) {
content.textContent = data[cellId].displayValue || data[cellId].value;
if (data[cellId].formula) cell.dataset.formula = 'true';
}
cell.appendChild(content);
cell.style.gridArea = (row + 1) + ' / ' + (col + 2);
cell.onclick = (e) => { e.stopPropagation(); selectCell(cell, col, row); content.focus(); };
return cell;
}
function getCellId(col, row) { return colLetters[col] + row; }
function getCellValue(cellId) { return data[cellId]?.value || '0'; }
function selectCell(cell, col, row) {
if (selectedCell) selectedCell.classList.remove('selected');
selectedCell = cell; cell.classList.add('selected');
document.getElementById('cell-name').value = getCellId(col, row);
syncFormulaBarWithCell();
document.getElementById('status-message').textContent = `Editing ${getCellId(col, row)}`;
}
function syncFormulaBarWithCell() {
if (!selectedCell) return;
const col = parseInt(selectedCell.dataset.col), row = parseInt(selectedCell.dataset.row);
const cellId = getCellId(col, row), formulaBar = document.getElementById('formula-bar');
formulaBar.value = data[cellId]?.formula ? '=' + data[cellId].formula : (data[cellId]?.value || '');
}
function handleCellKeydown(e) {
const cell = e.target.parentElement, col = parseInt(cell.dataset.col), row = parseInt(cell.dataset.row);
if (e.key === 'F2') { e.preventDefault(); document.getElementById('formula-bar').focus(); return; }
if (['ArrowRight', 'ArrowLeft', 'ArrowDown', 'ArrowUp'].includes(e.key)) {
e.preventDefault();
let nextCol = col, nextRow = row;
if (e.key === 'ArrowRight' && col < numCols - 1) nextCol++;
else if (e.key === 'ArrowLeft' && col > 0) nextCol--;
else if (e.key === 'ArrowDown' && row < numRows) nextRow++;
else if (e.key === 'ArrowUp' && row > 1) nextRow--;
const nextCell = document.getElementById(getCellId(nextCol, nextRow));
if (nextCell) nextCell.querySelector('.cell-content').focus();
}
}
function updateCellValue(cell, col, row) {
const content = cell.querySelector('.cell-content'), cellId = getCellId(col, row);
const value = content.textContent;
if (!data[cellId]) data[cellId] = { value: '', formula: '', displayValue: '' };
data[cellId].value = value; data[cellId].displayValue = value;
}
function handleFormulaInput(e) {
if (e.key !== 'Enter' || !selectedCell) return;
const formulaBar = e.target, input = formulaBar.value.trim();
const content = selectedCell.querySelector('.cell-content');
const col = parseInt(selectedCell.dataset.col), row = parseInt(selectedCell.dataset.row);
const cellId = getCellId(col, row);
if (input.toLowerCase().startsWith('del')) {
handleDelete(input); formulaBar.value = ''; return;
}
let formula = '', result = '';
if (input.startsWith('=')) {
formula = input.slice(1);
result = evaluateFormula(formula, cellId);
content.textContent = result;
selectedCell.dataset.formula = 'true';
data[cellId] = { value: result, formula, displayValue: result };
recalculateDependents(cellId);
} else {
content.textContent = input;
if (selectedCell.dataset.formula) delete selectedCell.dataset.formula;
data[cellId] = { value: input, formula: '', displayValue: input };
}
formulaBar.value = ''; content.focus();
}
function evaluateFormula(formula, currentCellId) {
try {
// Replace cell references
formula = formula.replace(/[A-Z]\d+/g, (match) => {
const col = colLetters.indexOf(match[0]), row = parseInt(match.slice(1));
const value = getCellValue(getCellId(col, row));
return isNaN(value) ? `"${value}"` : value;
});
// Parse function
const funcMatch = formula.match(/^([A-Z]+)\((.+)\)$/i);
if (funcMatch) {
const funcName = funcMatch[1].toUpperCase(), argsStr = funcMatch[2];
const args = argsStr.split(',').map(arg => arg.trim().replace(/"/g, ''));
if (functions[funcName]) {
const firstArg = args[0];
if (firstArg.includes(':')) {
return functions[funcName](firstArg);
} else {
return functions[funcName](...args.map(arg => isNaN(arg) ? arg : parseFloat(arg)));
}
}
}
// Simple math
return eval(formula);
} catch {
return '#ERROR!';
}
}
function getRangeValues(range) {
const [start, end] = range.split(':');
const startCol = colLetters.indexOf(start[0]), startRow = parseInt(start.slice(1));
const endCol = colLetters.indexOf(end[0]), endRow = parseInt(end.slice(1));
const values = [];
for (let c = Math.min(startCol, endCol); c <= Math.max(startCol, endCol); c++) {
for (let r = Math.min(startRow, endRow); r <= Math.max(startRow, endRow); r++) {
values.push(getCellValue(getCellId(c, r)));
}
}
return values;
}
function evalCondition(condition) {
try { return eval(condition); } catch { return false; }
}
function recalculateDependents(cellId) {
Object.keys(data).forEach(id => {
if (data[id].formula && data[id].formula.includes(cellId)) {
const result = evaluateFormula(data[id].formula, id);
const cell = document.getElementById(id);
if (cell) {
cell.querySelector('.cell-content').textContent = result;
data[id].value = result; data[id].displayValue = result;
}
}
});
}
// Auto-fill
function startAutoFill(e, col, row) {
e.preventDefault(); e.stopPropagation();
isDragging = true; startCell = { col, row };
document.body.style.cursor = 'se-resize';
document.addEventListener('mousemove', handleAutoFillDrag);
document.addEventListener('mouseup', endAutoFill);
}
function handleAutoFillDrag(e) {
if (!isDragging) return;
const grid = document.getElementById('grid'), rect = grid.getBoundingClientRect();
const x = e.clientX - rect.left, y = e.clientY - rect.top;
let endCol = Math.floor((x - 50) / 120), endRow = Math.floor((y - 40) / 40) + 1;
endCol = Math.max(0, Math.min(endCol, numCols - 1));
endRow = Math.max(1, Math.min(endRow, numRows));
dragEndCell = { col: endCol, row: endRow };
highlightRange(startCell.col, startCell.row, endCol, endRow);
}
function endAutoFill() {
if (!isDragging || !startCell || !dragEndCell) return;
isDragging = false; document.body.style.cursor = 'default';
performAutoFill(startCell.col, startCell.row, dragEndCell.col, dragEndCell.row);
clearHighlights();
document.removeEventListener('mousemove', handleAutoFillDrag);
document.removeEventListener('mouseup', endAutoFill);
startCell = dragEndCell = null;
}
function highlightRange(sCol, sRow, eCol, eRow) {
clearHighlights();
const minC = Math.min(sCol, eCol), maxC = Math.max(sCol, eCol);
const minR = Math.min(sRow, eRow), maxR = Math.max(sRow, eRow);
for (let c = minC; c <= maxC; c++) for (let r = minR; r <= maxR; r++) {
const cell = document.getElementById(getCellId(c, r));
if (cell) cell.classList.add('bg-blue-100');
}
}
function clearHighlights() {
document.querySelectorAll('.cell').forEach(cell => cell.classList.remove('bg-blue-100'));
}
function performAutoFill(sCol, sRow, eCol, eRow) {
const startId = getCellId(sCol, sRow);
const startData = data[startId] || { value: '', formula: '' };
const isFormula = !!startData.formula;
const deltaCol = eCol - sCol, deltaRow = eRow - sRow;
const dirCol = deltaCol !== 0 ? (deltaCol > 0 ? 1 : -1) : 0;
const dirRow = deltaRow !== 0 ? (deltaRow > 0 ? 1 : -1) : 0;
let step = 1;
let currentCol = sCol + dirCol, currentRow = sRow + dirRow;
while (dirCol ? (dirCol > 0 ? currentCol <= eCol : currentCol >= eCol) : true &&
dirRow ? (dirRow > 0 ? currentRow <= eRow : currentRow >= eRow) : true) {
const cellId = getCellId(currentCol, currentRow);
const cell = document.getElementById(cellId);
if (cell) {
let newValue, newFormula = '';
if (isFormula) {
newFormula = adjustFormulaReferences(startData.formula, step * dirRow, step * dirCol);
newValue = evaluateFormula(newFormula, cellId);
} else {
const startValue = parseFloat(startData.value);
if (!isNaN(startValue)) {
newValue = startValue + step * (dirRow || dirCol);
} else {
newValue = startData.value;
}
}
cell.querySelector('.cell-content').textContent = newValue;
data[cellId] = { value: newValue, formula: newFormula, displayValue: newValue };
if (newFormula) cell.dataset.formula = 'true';
}
currentCol += dirCol; currentRow += dirRow; step++;
}
}
function adjustFormulaReferences(formula, deltaRow, deltaCol) {
return formula.replace(/([A-Z])(\d+)/g, (match, colLetter, rowStr) => {
const col = colLetters.indexOf(colLetter);
const row = parseInt(rowStr);
const newCol = col + deltaCol;
const newRow = row + deltaRow;
return (newCol >= 0 ? colLetters[newCol] : colLetter) + newRow;
});
}
// Other functions
function toggleFunctionList() { document.getElementById('function-list').classList.toggle('hidden'); }
function insertFunction(name) {
document.getElementById('formula-bar').value += `=${name}(`;
document.getElementById('formula-bar').focus();
toggleFunctionList();
}
function handleDelete(input) {
const parts = input.toLowerCase().split(/\s+/);
if (parts.length === 1) {
selectedCell.querySelector('.cell-content').textContent = '';
delete data[selectedCell.id]; delete selectedCell.dataset.formula;
} else if (parts.length === 2) {
const target = parts[1].toUpperCase();
if (colLetters.includes(target[0])) deleteColumn(colLetters.indexOf(target[0]));
else if (!isNaN(target)) deleteRow(parseInt(target));
}
}
function deleteColumn(col) {
for (let r = 1; r <= numRows; r++) delete data[getCellId(col, r)];
for (let c = col; c < numCols - 1; c++) for (let r = 1; r <= numRows; r++) {
const oldId = getCellId(c + 1, r), newId = getCellId(c, r);
if (data[oldId]) { data[newId] = data[oldId]; delete data[oldId]; }
}
numCols--; initGrid();
}
function deleteRow(row) {
for (let c = 0; c < numCols; c++) delete data[getCellId(c, row)];
for (let r = row; r < numRows; r++) for (let c = 0; c < numCols; c++) {
const oldId = getCellId(c, r + 1), newId = getCellId(c, r);
if (data[oldId]) { data[newId] = data[oldId]; delete data[oldId]; }
}
numRows--; initGrid();
}
function addColumn() { numCols++; initGrid(); }
function addRow() { numRows++; initGrid(); }
function saveData() { localStorage.setItem('spreadmaster', JSON.stringify({data, numRows, numCols})); }
function loadData() {
const saved = localStorage.getItem('spreadmaster');
if (saved) {
const {data: d, numRows: r, numCols: c} = JSON.parse(saved);
data = d || {}; numRows = r || 20; numCols = c || 10;
}
}
// Selection helpers
function selectColumn(col) { document.getElementById('status-message').textContent = `Column ${colLetters[col]}`; }
function selectRow(row) { document.getElementById('status-message').textContent = `Row ${row}`; }
</script>
<script>feather.replace();</script>
</body>
</html>