This commit is contained in:
Artyom Tsyrulnikov
2025-07-13 12:15:17 +03:00
parent 79e5a07376
commit 14bff20150

View File

@@ -1,4 +1,4 @@
import { FC, useState } from 'react'; import { FC, useEffect, useRef, useState } from 'react';
interface CellData { interface CellData {
value: string; value: string;
@@ -14,11 +14,95 @@ const Spreadsheet: FC = () => {
}); });
const [selectedCell, setSelectedCell] = useState<{row: number, col: number} | null>(null); const [selectedCell, setSelectedCell] = useState<{row: number, col: number} | null>(null);
const [isEditing, setIsEditing] = useState(false);
const [editingValue, setEditingValue] = useState<string>(''); // Для отмены изменений
const [formulaBarValue, setFormulaBarValue] = useState<string>(''); // Значение в formula bar
const [columnWidths, setColumnWidths] = useState<number[]>(Array(10).fill(96)); // 96px = w-24 const [columnWidths, setColumnWidths] = useState<number[]>(Array(10).fill(96)); // 96px = w-24
const [rowHeights, setRowHeights] = useState<number[]>(Array(20).fill(32)); // 32px = h-8 const [rowHeights, setRowHeights] = useState<number[]>(Array(20).fill(32)); // 32px = h-8
const containerRef = useRef<HTMLDivElement>(null);
const inputRefs = useRef<(HTMLInputElement | null)[][]>([]);
const formulaBarRef = useRef<HTMLInputElement>(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) => { const handleCellClick = (row: number, col: number) => {
setSelectedCell({ row, col }); 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) => { const handleCellChange = (row: number, col: number, value: string) => {
@@ -27,6 +111,10 @@ const Spreadsheet: FC = () => {
newCells[row][col] = { ...newCells[row][col], value }; newCells[row][col] = { ...newCells[row][col], value };
return newCells; return newCells;
}); });
// Обновляем formula bar если изменяется текущая выбранная ячейка
if (selectedCell && selectedCell.row === row && selectedCell.col === col) {
setFormulaBarValue(value);
}
}; };
const getColumnLabel = (index: number) => { 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<string, [number, number]> = {
'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 ( return (
<div className="w-full h-full bg-white"> <div
ref={containerRef}
className="w-full h-full bg-white outline-none"
tabIndex={0}
onKeyDown={handleKeyDown}
>
{/* Formula bar */} {/* Formula bar */}
<div className="border-b border-gray-200 p-2 bg-white"> <div className="border-b border-gray-200 p-2 bg-white">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@@ -59,15 +253,37 @@ const Spreadsheet: FC = () => {
</div> </div>
<div className="flex-1"> <div className="flex-1">
<input <input
ref={formulaBarRef}
type="text" type="text"
className="w-full px-2 py-1 text-sm border border-gray-300 rounded focus:outline-none focus:ring-2 focus:ring-blue-500" className={`w-full px-2 py-1 text-sm border rounded focus:outline-none ${
isEditing
? 'border-blue-400 focus:ring-2 focus:ring-blue-300'
: 'border-gray-300 focus:ring-2 focus:ring-blue-500'
}`}
placeholder="Введите формулу..." placeholder="Введите формулу..."
value={selectedCell ? cells[selectedCell.row][selectedCell.col].value : ''} value={formulaBarValue}
onChange={(e) => { onChange={(e) => {
setFormulaBarValue(e.target.value);
if (selectedCell) { if (selectedCell) {
handleCellChange(selectedCell.row, selectedCell.col, e.target.value); 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);
}
}}
/> />
</div> </div>
</div> </div>
@@ -150,20 +366,79 @@ const Spreadsheet: FC = () => {
{row.map((cell, colIndex) => ( {row.map((cell, colIndex) => (
<td <td
key={colIndex} key={colIndex}
className={`border border-gray-300 relative cursor-cell ${ className={`border border-gray-300 relative ${
selectedCell?.row === rowIndex && selectedCell?.col === colIndex selectedCell?.row === rowIndex && selectedCell?.col === colIndex
? 'bg-blue-100 border-blue-500 border-2' ? ''
: 'hover:bg-gray-50' : 'hover:bg-gray-50'
}`} }`}
style={{ width: columnWidths[colIndex], height: rowHeights[rowIndex] }} style={{
onClick={() => handleCellClick(rowIndex, colIndex)} 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)}
> >
<input <input
type="text" type="text"
className="w-full h-full px-1 text-sm bg-transparent border-none outline-none resize-none" className="w-full h-full px-1 text-sm bg-transparent border-none outline-none resize-none cursor-text"
value={cell.value} value={cell.value}
onChange={(e) => handleCellChange(rowIndex, colIndex, e.target.value)} onChange={(e) => {
onFocus={() => setSelectedCell({ row: rowIndex, col: colIndex })} 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;
}
}}
/> />
</td> </td>
))} ))}