moving
This commit is contained in:
@@ -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<string>(''); // Для отмены изменений
|
||||
const [formulaBarValue, setFormulaBarValue] = useState<string>(''); // Значение в formula bar
|
||||
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 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) => {
|
||||
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<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 (
|
||||
<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 */}
|
||||
<div className="border-b border-gray-200 p-2 bg-white">
|
||||
<div className="flex items-center space-x-2">
|
||||
@@ -59,15 +253,37 @@ const Spreadsheet: FC = () => {
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<input
|
||||
ref={formulaBarRef}
|
||||
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="Введите формулу..."
|
||||
value={selectedCell ? cells[selectedCell.row][selectedCell.col].value : ''}
|
||||
value={formulaBarValue}
|
||||
onChange={(e) => {
|
||||
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);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -150,20 +366,79 @@ const Spreadsheet: FC = () => {
|
||||
{row.map((cell, colIndex) => (
|
||||
<td
|
||||
key={colIndex}
|
||||
className={`border border-gray-300 relative cursor-cell ${
|
||||
className={`border border-gray-300 relative ${
|
||||
selectedCell?.row === rowIndex && selectedCell?.col === colIndex
|
||||
? 'bg-blue-100 border-blue-500 border-2'
|
||||
? ''
|
||||
: 'hover:bg-gray-50'
|
||||
}`}
|
||||
style={{ width: columnWidths[colIndex], height: rowHeights[rowIndex] }}
|
||||
onClick={() => 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)}
|
||||
>
|
||||
<input
|
||||
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}
|
||||
onChange={(e) => 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;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user