This commit is contained in:
Artyom Tsyrulnikov
2025-07-13 13:08:01 +03:00
parent 14bff20150
commit 36bdb4b2be
6 changed files with 1074 additions and 11 deletions

View File

@@ -1,4 +1,5 @@
import { FC, useEffect, useRef, useState } from 'react';
import { useSpreadsheetEngine } from '../../hooks/useSpreadsheetEngine';
interface CellData {
value: string;
@@ -6,6 +7,19 @@ interface CellData {
}
const Spreadsheet: FC = () => {
const {
cells: engineCells,
setCellValue: setEngineCellValue,
getCellValue,
getCellRawValue,
isCalculating,
recalculate
} = useSpreadsheetEngine({
rows: 20,
cols: 10,
sheetName: 'Sheet1'
});
const [cells, setCells] = useState<CellData[][]>(() => {
// Создаем сетку 20x10 с пустыми ячейками
return Array(20).fill(null).map(() =>
@@ -28,14 +42,50 @@ const Spreadsheet: FC = () => {
inputRefs.current = Array(20).fill(null).map(() => Array(10).fill(null));
}, []);
// Синхронизируем локальное состояние с движком
useEffect(() => {
setCells(prev => {
const newCells = [...prev];
for (let row = 0; row < 20; row++) {
for (let col = 0; col < 10; col++) {
const rawValue = getCellRawValue(row, col);
if (rawValue !== newCells[row][col].value) {
newCells[row][col] = { ...newCells[row][col], value: rawValue };
}
}
}
return newCells;
});
}, [engineCells, getCellRawValue]);
// Тестовые данные для демонстрации
useEffect(() => {
// Добавляем тестовые данные
setEngineCellValue(0, 0, '10'); // A1 = 10
setEngineCellValue(1, 0, '20'); // A2 = 20
setEngineCellValue(2, 0, '=A1+A2'); // A3 = A1+A2
setEngineCellValue(0, 1, '=A1*2'); // B1 = A1*2
setEngineCellValue(1, 1, '=SUM(A1,A2)'); // B2 = SUM(A1,A2)
setEngineCellValue(0, 2, '="Результат: " + A3'); // C1 = "Результат: " + A3
// Демонстрация волатильных функций
setEngineCellValue(0, 3, '=RAND()'); // D1 = случайное число
setEngineCellValue(1, 3, '=RANDBETWEEN(1,100)'); // D2 = случайное число от 1 до 100
setEngineCellValue(2, 3, '=D1*100'); // D3 = D1*100 (зависит от волатильной функции)
setEngineCellValue(0, 4, '=TODAY()'); // E1 = сегодняшняя дата
setEngineCellValue(1, 4, '=NOW()'); // E2 = текущая дата и время
}, [setEngineCellValue]);
// Синхронизируем значение formula bar с выбранной ячейкой
useEffect(() => {
if (selectedCell) {
setFormulaBarValue(cells[selectedCell.row][selectedCell.col].value);
// Показываем сырое значение (формулу) в formula bar
const rawValue = getCellRawValue(selectedCell.row, selectedCell.col);
setFormulaBarValue(rawValue);
} else {
setFormulaBarValue('');
}
}, [selectedCell, cells]);
}, [selectedCell, getCellRawValue, engineCells]);
const handleCellClick = (row: number, col: number) => {
setSelectedCell({ row, col });
@@ -45,7 +95,7 @@ const Spreadsheet: FC = () => {
const handleCellDoubleClick = (row: number, col: number) => {
setSelectedCell({ row, col });
// Сохраняем текущее значение для возможности отмены
// Сохраняем текущее значение из UI для возможности отмены
setEditingValue(cells[row][col].value);
setIsEditing(true);
// Фокус на input ячейки и курсор в конец
@@ -56,7 +106,7 @@ const Spreadsheet: FC = () => {
// Устанавливаем курсор в конец текста
input.setSelectionRange(input.value.length, input.value.length);
}
}, 0);
}, 10); // Увеличиваем задержку для надежности
};
const moveSelection = (deltaRow: number, deltaCol: number) => {
@@ -71,7 +121,7 @@ const Spreadsheet: FC = () => {
const startEditing = (focusTarget?: 'cell' | 'formula') => {
if (!selectedCell) return;
// Сохраняем текущее значение для возможности отмены
// Сохраняем текущее значение из UI для возможности отмены
setEditingValue(cells[selectedCell.row][selectedCell.col].value);
setIsEditing(true);
@@ -98,23 +148,37 @@ const Spreadsheet: FC = () => {
const stopEditing = (save: boolean = true) => {
if (!save && selectedCell) {
// Отменяем изменения - возвращаем старое значение
handleCellChange(selectedCell.row, selectedCell.col, editingValue);
setCells(prev => {
const newCells = [...prev];
newCells[selectedCell.row][selectedCell.col] = {
...newCells[selectedCell.row][selectedCell.col],
value: editingValue
};
return newCells;
});
setFormulaBarValue(editingValue);
// Восстанавливаем значение в движке
setEngineCellValue(selectedCell.row, selectedCell.col, editingValue);
}
setIsEditing(false);
containerRef.current?.focus();
};
const handleCellChange = (row: number, col: number, value: string) => {
// Обновляем локальное состояние для UI (сырое значение)
setCells(prev => {
const newCells = [...prev];
newCells[row][col] = { ...newCells[row][col], value };
return newCells;
});
// Обновляем formula bar если изменяется текущая выбранная ячейка
if (selectedCell && selectedCell.row === row && selectedCell.col === col) {
setFormulaBarValue(value);
}
// Обновляем значение в движке (это может быть медленно, поэтому делаем в конце)
setEngineCellValue(row, col, value);
};
const getColumnLabel = (index: number) => {
@@ -178,7 +242,7 @@ const Spreadsheet: FC = () => {
e.preventDefault();
moveSelection(0, 1);
break;
case 'Enter':
case 'Enter':
e.preventDefault();
if (selectedCell) {
setEditingValue(cells[selectedCell.row][selectedCell.col].value);
@@ -193,7 +257,7 @@ const Spreadsheet: FC = () => {
}, 0);
}
break;
case 'F2':
case 'F2':
e.preventDefault();
if (selectedCell) {
setEditingValue(cells[selectedCell.row][selectedCell.col].value);
@@ -251,6 +315,11 @@ const Spreadsheet: FC = () => {
<div className="text-sm text-gray-600 min-w-[60px]">
{selectedCell ? `${getColumnLabel(selectedCell.col)}${selectedCell.row + 1}` : ''}
</div>
{isCalculating && (
<div className="text-xs text-blue-600 animate-pulse">
Вычисляется...
</div>
)}
<div className="flex-1">
<input
ref={formulaBarRef}
@@ -382,6 +451,11 @@ const Spreadsheet: FC = () => {
: {})
}}
onClick={() => {
// Если в режиме редактирования и это активная ячейка, не обрабатываем клик
if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex) {
return;
}
handleCellClick(rowIndex, colIndex);
// Фокусируемся на контейнере, чтобы работали клавиши
containerRef.current?.focus();
@@ -390,8 +464,14 @@ const Spreadsheet: FC = () => {
>
<input
type="text"
className="w-full h-full px-1 text-sm bg-transparent border-none outline-none resize-none cursor-text"
value={cell.value}
className={`w-full h-full px-1 text-sm bg-transparent border-none outline-none resize-none ${
isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex
? 'cursor-text'
: 'cursor-cell'
}`}
value={isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex
? cell.value
: String(getCellValue(rowIndex, colIndex) || '')}
onChange={(e) => {
handleCellChange(rowIndex, colIndex, e.target.value);
setFormulaBarValue(e.target.value);
@@ -416,6 +496,11 @@ const Spreadsheet: FC = () => {
}
}}
onClick={(e) => {
// Если в режиме редактирования и это активная ячейка, не обрабатываем клик
if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex) {
return;
}
e.stopPropagation();
setSelectedCell({ row: rowIndex, col: colIndex });
setIsEditing(false); // Явно отключаем режим редактирования при клике
@@ -430,9 +515,22 @@ const Spreadsheet: FC = () => {
// Фокус уже на input, устанавливаем курсор в конец
setTimeout(() => {
const input = e.target as HTMLInputElement;
input.focus();
input.setSelectionRange(input.value.length, input.value.length);
}, 0);
}}
onMouseDown={(e) => {
// В режиме редактирования разрешаем нормальное поведение мыши
if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex) {
e.stopPropagation();
}
}}
onMouseUp={(e) => {
// В режиме редактирования разрешаем нормальное поведение мыши
if (isEditing && selectedCell?.row === rowIndex && selectedCell?.col === colIndex) {
e.stopPropagation();
}
}}
readOnly={!isEditing || selectedCell?.row !== rowIndex || selectedCell?.col !== colIndex}
ref={(el) => {
if (inputRefs.current[rowIndex]) {