moving
This commit is contained in:
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
Reference in New Issue
Block a user