From 14bff20150355e234974f40c8d60b1252a42bf3e Mon Sep 17 00:00:00 2001 From: Artyom Tsyrulnikov Date: Sun, 13 Jul 2025 12:15:17 +0300 Subject: [PATCH] moving --- src/components/Spreadsheet/Spreadsheet.tsx | 297 ++++++++++++++++++++- 1 file changed, 286 insertions(+), 11 deletions(-) diff --git a/src/components/Spreadsheet/Spreadsheet.tsx b/src/components/Spreadsheet/Spreadsheet.tsx index b3dd0bd..03184e3 100644 --- a/src/components/Spreadsheet/Spreadsheet.tsx +++ b/src/components/Spreadsheet/Spreadsheet.tsx @@ -1,4 +1,4 @@ -import { FC, useState } from 'react'; +import { FC, useEffect, useRef, useState } from 'react'; interface CellData { value: string; @@ -14,11 +14,95 @@ const Spreadsheet: FC = () => { }); const [selectedCell, setSelectedCell] = useState<{row: number, col: number} | null>(null); + const [isEditing, setIsEditing] = useState(false); + const [editingValue, setEditingValue] = useState(''); // Для отмены изменений + const [formulaBarValue, setFormulaBarValue] = useState(''); // Значение в formula bar const [columnWidths, setColumnWidths] = useState(Array(10).fill(96)); // 96px = w-24 const [rowHeights, setRowHeights] = useState(Array(20).fill(32)); // 32px = h-8 + const containerRef = useRef(null); + const inputRefs = useRef<(HTMLInputElement | null)[][]>([]); + const formulaBarRef = useRef(null); + + // Инициализируем refs для всех input + useEffect(() => { + inputRefs.current = Array(20).fill(null).map(() => Array(10).fill(null)); + }, []); + + // Синхронизируем значение formula bar с выбранной ячейкой + useEffect(() => { + if (selectedCell) { + setFormulaBarValue(cells[selectedCell.row][selectedCell.col].value); + } else { + setFormulaBarValue(''); + } + }, [selectedCell, cells]); const handleCellClick = (row: number, col: number) => { setSelectedCell({ row, col }); + setIsEditing(false); + containerRef.current?.focus(); + }; + + const handleCellDoubleClick = (row: number, col: number) => { + setSelectedCell({ row, col }); + // Сохраняем текущее значение для возможности отмены + setEditingValue(cells[row][col].value); + setIsEditing(true); + // Фокус на input ячейки и курсор в конец + setTimeout(() => { + const input = inputRefs.current[row]?.[col]; + if (input) { + input.focus(); + // Устанавливаем курсор в конец текста + input.setSelectionRange(input.value.length, input.value.length); + } + }, 0); + }; + + const moveSelection = (deltaRow: number, deltaCol: number) => { + if (!selectedCell) return; + + const newRow = Math.max(0, Math.min(19, selectedCell.row + deltaRow)); + const newCol = Math.max(0, Math.min(9, selectedCell.col + deltaCol)); + + setSelectedCell({ row: newRow, col: newCol }); + setIsEditing(false); + }; + + const startEditing = (focusTarget?: 'cell' | 'formula') => { + if (!selectedCell) return; + // Сохраняем текущее значение для возможности отмены + setEditingValue(cells[selectedCell.row][selectedCell.col].value); + setIsEditing(true); + + // Фокус в зависимости от того, откуда вызвано редактирование + if (focusTarget === 'cell') { + setTimeout(() => { + const input = inputRefs.current[selectedCell.row]?.[selectedCell.col]; + if (input) { + input.focus(); + input.select(); + } + }, 0); + } else if (focusTarget === 'formula') { + setTimeout(() => { + if (formulaBarRef.current) { + formulaBarRef.current.focus(); + formulaBarRef.current.select(); + } + }, 0); + } + // Если focusTarget не указан, не меняем фокус + }; + + const stopEditing = (save: boolean = true) => { + if (!save && selectedCell) { + // Отменяем изменения - возвращаем старое значение + handleCellChange(selectedCell.row, selectedCell.col, editingValue); + setFormulaBarValue(editingValue); + } + setIsEditing(false); + containerRef.current?.focus(); }; const handleCellChange = (row: number, col: number, value: string) => { @@ -27,6 +111,10 @@ const Spreadsheet: FC = () => { newCells[row][col] = { ...newCells[row][col], value }; return newCells; }); + // Обновляем formula bar если изменяется текущая выбранная ячейка + if (selectedCell && selectedCell.row === row && selectedCell.col === col) { + setFormulaBarValue(value); + } }; const getColumnLabel = (index: number) => { @@ -49,8 +137,114 @@ const Spreadsheet: FC = () => { }); }; + const handleKeyDown = (e: React.KeyboardEvent) => { + if (!selectedCell) return; + + // Если в режиме редактирования, обрабатываем Esc и стрелочки + if (isEditing) { + if (e.key === 'Escape') { + e.preventDefault(); + stopEditing(false); // Отменяем изменения + } else if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { + e.preventDefault(); + stopEditing(true); // Сохраняем изменения + // Затем перемещаемся + const deltaMap: Record = { + 'ArrowUp': [-1, 0], + 'ArrowDown': [1, 0], + 'ArrowLeft': [0, -1], + 'ArrowRight': [0, 1] + }; + const [deltaRow, deltaCol] = deltaMap[e.key]; + moveSelection(deltaRow, deltaCol); + } + return; + } + + switch (e.key) { + case 'ArrowUp': + e.preventDefault(); + moveSelection(-1, 0); + break; + case 'ArrowDown': + e.preventDefault(); + moveSelection(1, 0); + break; + case 'ArrowLeft': + e.preventDefault(); + moveSelection(0, -1); + break; + case 'ArrowRight': + e.preventDefault(); + moveSelection(0, 1); + break; + case 'Enter': + e.preventDefault(); + if (selectedCell) { + setEditingValue(cells[selectedCell.row][selectedCell.col].value); + setIsEditing(true); + setTimeout(() => { + const input = inputRefs.current[selectedCell.row]?.[selectedCell.col]; + if (input) { + input.focus(); + // Устанавливаем курсор в конец текста + input.setSelectionRange(input.value.length, input.value.length); + } + }, 0); + } + break; + case 'F2': + e.preventDefault(); + if (selectedCell) { + setEditingValue(cells[selectedCell.row][selectedCell.col].value); + setIsEditing(true); + setTimeout(() => { + const input = inputRefs.current[selectedCell.row]?.[selectedCell.col]; + if (input) { + input.focus(); + // Устанавливаем курсор в конец текста + input.setSelectionRange(input.value.length, input.value.length); + } + }, 0); + } + break; + case 'Backspace': + case 'Delete': + e.preventDefault(); + if (selectedCell) { + handleCellChange(selectedCell.row, selectedCell.col, ''); + } + break; + default: + // Если нажата обычная клавиша (буква/цифра), начинаем редактирование + if (e.key.length === 1 && !e.ctrlKey && !e.altKey && !e.metaKey) { + e.preventDefault(); + if (selectedCell) { + handleCellChange(selectedCell.row, selectedCell.col, e.key); + setFormulaBarValue(e.key); + setEditingValue(cells[selectedCell.row][selectedCell.col].value); + setIsEditing(true); + setTimeout(() => { + const input = inputRefs.current[selectedCell.row]?.[selectedCell.col]; + if (input) { + input.focus(); + // Устанавливаем курсор в конец + input.setSelectionRange(input.value.length, input.value.length); + } + }, 0); + } + } + break; + } + }; + return ( -
+
{/* Formula bar */}
@@ -59,15 +253,37 @@ const Spreadsheet: FC = () => {
{ + setFormulaBarValue(e.target.value); if (selectedCell) { handleCellChange(selectedCell.row, selectedCell.col, e.target.value); } }} + onFocus={() => { + if (selectedCell) { + startEditing('formula'); + } + }} + onBlur={() => stopEditing(true)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + stopEditing(true); + containerRef.current?.focus(); + } else if (e.key === 'Escape') { + e.preventDefault(); + stopEditing(false); + } + }} />
@@ -150,20 +366,79 @@ const Spreadsheet: FC = () => { {row.map((cell, colIndex) => ( handleCellClick(rowIndex, colIndex)} + style={{ + width: columnWidths[colIndex], + height: rowHeights[rowIndex], + ...(selectedCell?.row === rowIndex && selectedCell?.col === colIndex + ? { boxShadow: isEditing + ? 'inset 0 0 0 2px #3b82f6, inset 0 0 0 4px rgba(59, 130, 246, 0.3)' // Синяя рамка + полупрозрачная голубая вокруг + : 'inset 0 0 0 2px #3b82f6' // Только синяя рамка в режиме выделения + } + : {}) + }} + onClick={() => { + handleCellClick(rowIndex, colIndex); + // Фокусируемся на контейнере, чтобы работали клавиши + containerRef.current?.focus(); + }} + onDoubleClick={() => handleCellDoubleClick(rowIndex, colIndex)} > handleCellChange(rowIndex, colIndex, e.target.value)} - onFocus={() => setSelectedCell({ row: rowIndex, col: colIndex })} + onChange={(e) => { + handleCellChange(rowIndex, colIndex, e.target.value); + setFormulaBarValue(e.target.value); + }} + onFocus={(e) => { + // Только устанавливаем выбранную ячейку, не включаем режим редактирования автоматически + setSelectedCell({ row: rowIndex, col: colIndex }); + // Если не в режиме редактирования, убираем фокус + if (!isEditing) { + (e.target as HTMLInputElement).blur(); + } + }} + onBlur={() => stopEditing(true)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + e.preventDefault(); + stopEditing(true); + moveSelection(1, 0); + } else if (e.key === 'Escape') { + e.preventDefault(); + stopEditing(false); + } + }} + onClick={(e) => { + e.stopPropagation(); + setSelectedCell({ row: rowIndex, col: colIndex }); + setIsEditing(false); // Явно отключаем режим редактирования при клике + // Убираем фокус с input чтобы не мигал курсор + (e.target as HTMLInputElement).blur(); + }} + onDoubleClick={(e) => { + e.stopPropagation(); + setSelectedCell({ row: rowIndex, col: colIndex }); + setEditingValue(cells[rowIndex][colIndex].value); + setIsEditing(true); + // Фокус уже на input, устанавливаем курсор в конец + setTimeout(() => { + const input = e.target as HTMLInputElement; + input.setSelectionRange(input.value.length, input.value.length); + }, 0); + }} + readOnly={!isEditing || selectedCell?.row !== rowIndex || selectedCell?.col !== colIndex} + ref={(el) => { + if (inputRefs.current[rowIndex]) { + inputRefs.current[rowIndex][colIndex] = el; + } + }} /> ))}