выделение ячеек
This commit is contained in:
@@ -16,6 +16,7 @@ import { FormulaBar, Spreadsheet } from './components'
|
||||
import { useDraggableDivider } from './hooks/useDraggableDivider'
|
||||
import {
|
||||
CachedCellData,
|
||||
CellSelection,
|
||||
COL_COUNT,
|
||||
DualSpreadsheetProps,
|
||||
HighlightedCell,
|
||||
@@ -28,7 +29,9 @@ import {
|
||||
convertReferencesToHighlights,
|
||||
createCellAddress,
|
||||
createQualifiedCellReference,
|
||||
createSelectionFromRange,
|
||||
extractCellReferences,
|
||||
getAllCellsInSelections,
|
||||
getColumnLabel,
|
||||
} from './utils'
|
||||
|
||||
@@ -75,6 +78,13 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
row: number
|
||||
col: number
|
||||
} | null>(null)
|
||||
// Состояние для множественного выделения
|
||||
const [multiSelection, setMultiSelection] = useState<CellSelection[]>([])
|
||||
const [isDragging, setIsDragging] = useState(false)
|
||||
const [dragStart, setDragStart] = useState<{
|
||||
row: number
|
||||
col: number
|
||||
} | null>(null)
|
||||
const [isEditing, setIsEditing] = useState(false)
|
||||
const [editingValue, setEditingValue] = useState<string>('') // Локальное значение во время редактирования
|
||||
// Источник ввода: 'cell' (редактирование внутри ячейки) или 'formula'
|
||||
@@ -396,6 +406,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
|
||||
const startEditing = useCallback(() => {
|
||||
if (!selectedCell) return
|
||||
// Сбрасываем множественное выделение при начале редактирования
|
||||
setMultiSelection([])
|
||||
const sheetName = SHEET_NAMES[activeSheet]
|
||||
const rawValue = multiSheetEngine.getCellRawValue(
|
||||
sheetName,
|
||||
@@ -465,6 +477,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
)
|
||||
updateRevision()
|
||||
multiSheetEngine.debouncedRecalc()
|
||||
// Сбрасываем множественное выделение при очистке одиночной ячейки
|
||||
setMultiSelection([])
|
||||
}, [activeSheet, selectedCell, multiSheetEngine, updateRevision])
|
||||
|
||||
const moveSelection = useCallback(
|
||||
@@ -488,10 +502,30 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
)
|
||||
|
||||
setSelectedCell({ row: newRow, col: newCol })
|
||||
// Сбрасываем множественное выделение при навигации
|
||||
setMultiSelection([])
|
||||
},
|
||||
[selectedCell, isEditing, stopEditing]
|
||||
)
|
||||
|
||||
// Функция для очистки множества выделенных ячеек
|
||||
const handleClearMultipleCells = useCallback(() => {
|
||||
if (multiSelection.length === 0) return
|
||||
|
||||
const sheetName = SHEET_NAMES[activeSheet]
|
||||
const cellsToDelete = getAllCellsInSelections(multiSelection)
|
||||
|
||||
cellsToDelete.forEach(({ row, col }: { row: number; col: number }) => {
|
||||
multiSheetEngine.setCellValueWithoutRecalc(sheetName, row, col, '')
|
||||
})
|
||||
|
||||
updateRevision()
|
||||
multiSheetEngine.debouncedRecalc()
|
||||
|
||||
// Очищаем выделение после удаления
|
||||
setMultiSelection([])
|
||||
}, [multiSelection, activeSheet, multiSheetEngine, updateRevision])
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (isEditing) {
|
||||
@@ -518,8 +552,39 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
setEditingSource('cell')
|
||||
startEditing()
|
||||
},
|
||||
Delete: handleClearCell,
|
||||
Backspace: handleClearCell,
|
||||
Delete: () => {
|
||||
// Если есть множественное выделение, удаляем все выделенные ячейки
|
||||
if (multiSelection.length > 0) {
|
||||
handleClearMultipleCells()
|
||||
} else {
|
||||
handleClearCell()
|
||||
}
|
||||
},
|
||||
Backspace: () => {
|
||||
// Если есть множественное выделение, удаляем все выделенные ячейки
|
||||
if (multiSelection.length > 0) {
|
||||
handleClearMultipleCells()
|
||||
} else {
|
||||
handleClearCell()
|
||||
}
|
||||
},
|
||||
Escape: () => {
|
||||
// Сбрасываем множественное выделение
|
||||
setMultiSelection([])
|
||||
},
|
||||
}
|
||||
|
||||
// Ctrl+A - выделить все ячейки
|
||||
if (e.key === 'a' && (e.ctrlKey || e.metaKey)) {
|
||||
e.preventDefault()
|
||||
const allSelection = createSelectionFromRange(
|
||||
0,
|
||||
0,
|
||||
ROW_COUNT - 1,
|
||||
COL_COUNT - 1
|
||||
)
|
||||
setMultiSelection([allSelection])
|
||||
return
|
||||
}
|
||||
|
||||
if (keyHandlers[e.key]) {
|
||||
@@ -532,11 +597,19 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
setIsEditing(true)
|
||||
}
|
||||
},
|
||||
[selectedCell, isEditing, moveSelection, startEditing, handleClearCell]
|
||||
[
|
||||
selectedCell,
|
||||
isEditing,
|
||||
moveSelection,
|
||||
startEditing,
|
||||
handleClearCell,
|
||||
multiSelection,
|
||||
handleClearMultipleCells,
|
||||
]
|
||||
)
|
||||
|
||||
const handleCellClick = useCallback(
|
||||
(row: number, col: number) => {
|
||||
(row: number, col: number, event?: React.MouseEvent) => {
|
||||
// console.log('🖱️ Обычный клик по ячейке:', {
|
||||
// row,
|
||||
//col,
|
||||
@@ -547,12 +620,93 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
if (isEditing) {
|
||||
stopEditing(true)
|
||||
}
|
||||
|
||||
// Если удерживается Ctrl (или Cmd на Mac), добавляем к множественному выделению
|
||||
if (event && (event.ctrlKey || event.metaKey)) {
|
||||
const newSelection = createSelectionFromRange(row, col, row, col)
|
||||
setMultiSelection(prev => [...prev, newSelection])
|
||||
setSelectedCell({ row, col })
|
||||
return
|
||||
}
|
||||
|
||||
// Если удерживается Shift, расширяем выделение
|
||||
if (event && event.shiftKey && selectedCell) {
|
||||
const newSelection = createSelectionFromRange(
|
||||
selectedCell.row,
|
||||
selectedCell.col,
|
||||
row,
|
||||
col
|
||||
)
|
||||
setMultiSelection([newSelection])
|
||||
return
|
||||
}
|
||||
|
||||
// Обычный клик - очищаем множественное выделение
|
||||
setMultiSelection([])
|
||||
setSelectedCell({ row, col })
|
||||
containerRefs.current[activeSheet]?.focus({ preventScroll: true })
|
||||
},
|
||||
[isEditing, stopEditing, activeSheet]
|
||||
[isEditing, stopEditing, activeSheet, selectedCell]
|
||||
)
|
||||
|
||||
// Новый обработчик для начала drag selection
|
||||
const handleCellMouseDown = useCallback(
|
||||
(row: number, col: number, event: React.MouseEvent) => {
|
||||
// Игнорируем правый клик
|
||||
if (event.button !== 0) return
|
||||
|
||||
// Если удерживаются модификаторы, не начинаем drag
|
||||
if (event.ctrlKey || event.metaKey || event.shiftKey) return
|
||||
|
||||
setIsDragging(true)
|
||||
setDragStart({ row, col })
|
||||
setMultiSelection([])
|
||||
setSelectedCell({ row, col })
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
// Обработчик для drag selection
|
||||
const handleCellMouseEnter = useCallback(
|
||||
(row: number, col: number) => {
|
||||
if (!isDragging || !dragStart) return
|
||||
|
||||
const newSelection = createSelectionFromRange(
|
||||
dragStart.row,
|
||||
dragStart.col,
|
||||
row,
|
||||
col
|
||||
)
|
||||
setMultiSelection([newSelection])
|
||||
},
|
||||
[isDragging, dragStart]
|
||||
)
|
||||
|
||||
// Обработчик завершения drag selection
|
||||
const handleCellMouseUp = useCallback(() => {
|
||||
if (isDragging) {
|
||||
setIsDragging(false)
|
||||
setDragStart(null)
|
||||
}
|
||||
}, [isDragging])
|
||||
|
||||
// Глобальный обработчик для завершения drag selection
|
||||
useEffect(() => {
|
||||
const handleGlobalMouseUp = () => {
|
||||
if (isDragging) {
|
||||
setIsDragging(false)
|
||||
setDragStart(null)
|
||||
}
|
||||
}
|
||||
|
||||
if (isDragging) {
|
||||
document.addEventListener('mouseup', handleGlobalMouseUp)
|
||||
return () => {
|
||||
document.removeEventListener('mouseup', handleGlobalMouseUp)
|
||||
}
|
||||
}
|
||||
}, [isDragging])
|
||||
|
||||
// Новый обработчик для кликов в режиме редактирования - вставляет ссылку
|
||||
const handleCellReferenceClick = useCallback(
|
||||
(targetSheetType: SheetType, row: number, col: number) => {
|
||||
@@ -940,6 +1094,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
spreadsheetWidths={spreadsheetWidths}
|
||||
getCachedCellData={getCachedCellData}
|
||||
selectedCell={selectedCell}
|
||||
multiSelection={multiSelection}
|
||||
activeSheet={activeSheet}
|
||||
isEditing={isEditing}
|
||||
editingValue={editingValue}
|
||||
@@ -948,6 +1103,9 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
|
||||
handleCellDoubleClick={handleCellDoubleClick}
|
||||
handleCellChange={handleCellChange}
|
||||
handleCellReferenceClick={handleCellReferenceClick}
|
||||
handleCellMouseDown={handleCellMouseDown}
|
||||
handleCellMouseEnter={handleCellMouseEnter}
|
||||
handleCellMouseUp={handleCellMouseUp}
|
||||
isInEditMode={isEditing}
|
||||
stopEditing={stopEditing}
|
||||
setActiveSheet={setActiveSheet}
|
||||
|
||||
@@ -9,6 +9,12 @@ export const Cell = memo(
|
||||
columnWidth,
|
||||
columnWidths,
|
||||
isSelected,
|
||||
isInMultiSelection,
|
||||
multiSelectionActive,
|
||||
selectionTop = false,
|
||||
selectionBottom = false,
|
||||
selectionLeft = false,
|
||||
selectionRight = false,
|
||||
isEditing,
|
||||
displayValue,
|
||||
rawValue,
|
||||
@@ -19,6 +25,9 @@ export const Cell = memo(
|
||||
onCellDoubleClick,
|
||||
onCellValueChange,
|
||||
onCellReferenceClick,
|
||||
onCellMouseDown,
|
||||
onCellMouseEnter,
|
||||
onCellMouseUp,
|
||||
isInEditMode,
|
||||
stopEditing,
|
||||
setActiveSheet,
|
||||
@@ -35,13 +44,29 @@ export const Cell = memo(
|
||||
// Определяем классы CSS для подсветки и обычного состояния
|
||||
let className = 'border border-border'
|
||||
|
||||
// Приоритет стилей: подсветка формул > изменения > обычное состояние
|
||||
// Базовый style для ячейки
|
||||
let cellStyle: CSSProperties = {
|
||||
...style,
|
||||
overflow: 'visible',
|
||||
padding: 0,
|
||||
userSelect: 'none',
|
||||
}
|
||||
|
||||
// Приоритет стилей: подсветка формул > множественное выделение > изменения > обычное состояние
|
||||
if (highlightColor) {
|
||||
className += ` ${highlightColor}`
|
||||
} else if (isModified && !mergedCellInfo?.isMerged) {
|
||||
className += ' bg-yellow-200'
|
||||
}
|
||||
|
||||
// Настраиваем box-shadow, фон и z-index для выделенной области
|
||||
if (isInMultiSelection) {
|
||||
cellStyle = {
|
||||
...cellStyle,
|
||||
backgroundColor: 'rgba(59,130,246,0.08)', // голубоватый оттенок
|
||||
}
|
||||
}
|
||||
|
||||
// Отладка стилей для измененных ячеек
|
||||
if (isModified) {
|
||||
// console.log(`🎨 Стили для измененной ячейки ${rowIndex},${colIndex}:`, //{
|
||||
@@ -54,13 +79,6 @@ export const Cell = memo(
|
||||
}
|
||||
|
||||
// Для объединенных ячеек нужно рассчитать правильные размеры
|
||||
let cellStyle: CSSProperties = {
|
||||
...style,
|
||||
overflow: 'visible',
|
||||
padding: 0,
|
||||
}
|
||||
|
||||
// Если это основная ячейка объединенной группы, расширяем её размеры
|
||||
if (
|
||||
mergedCellInfo?.isMainCell &&
|
||||
mergedCellInfo.rowSpan &&
|
||||
@@ -140,8 +158,7 @@ export const Cell = memo(
|
||||
//)
|
||||
}
|
||||
|
||||
setActiveSheet(sheetType)
|
||||
onCellClick(rowIndex, colIndex)
|
||||
onCellClick(rowIndex, colIndex, e)
|
||||
},
|
||||
[
|
||||
setActiveSheet,
|
||||
@@ -162,6 +179,11 @@ export const Cell = memo(
|
||||
|
||||
const handleMouseDown = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (!isInEditMode) {
|
||||
// блокируем стандартное выделение текста браузером
|
||||
e.preventDefault()
|
||||
}
|
||||
|
||||
// Используем mousedown, чтобы сработать раньше blur FormulaBar
|
||||
if (isInEditMode && onCellReferenceClick && !isSelected) {
|
||||
e.preventDefault()
|
||||
@@ -174,6 +196,12 @@ export const Cell = memo(
|
||||
onCellReferenceClick(sheetType, rowIndex, colIndex)
|
||||
return
|
||||
}
|
||||
|
||||
// Обработчик для начала drag selection
|
||||
if (onCellMouseDown) {
|
||||
setActiveSheet(sheetType)
|
||||
onCellMouseDown(rowIndex, colIndex, e)
|
||||
}
|
||||
},
|
||||
[
|
||||
isInEditMode,
|
||||
@@ -182,14 +210,30 @@ export const Cell = memo(
|
||||
sheetType,
|
||||
rowIndex,
|
||||
colIndex,
|
||||
onCellMouseDown,
|
||||
setActiveSheet,
|
||||
]
|
||||
)
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
if (onCellMouseEnter) {
|
||||
onCellMouseEnter(rowIndex, colIndex)
|
||||
}
|
||||
}, [onCellMouseEnter, rowIndex, colIndex])
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
if (onCellMouseUp) {
|
||||
onCellMouseUp()
|
||||
}
|
||||
}, [onCellMouseUp])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={className}
|
||||
style={cellStyle}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseUp={handleMouseUp}
|
||||
onClick={handleClick}
|
||||
onDoubleClick={handleDoubleClick}
|
||||
>
|
||||
@@ -226,7 +270,7 @@ export const Cell = memo(
|
||||
style={{
|
||||
overflow: availablePx > columnWidth ? 'visible' : 'hidden',
|
||||
whiteSpace: 'nowrap',
|
||||
...(isSelected
|
||||
...(isSelected && !multiSelectionActive
|
||||
? { boxShadow: 'inset 0 0 0 2px hsl(var(--primary))' }
|
||||
: highlightColor
|
||||
? { boxShadow: 'inset 0 0 0 2px currentColor' }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CellRendererProps } from '../types'
|
||||
import { findMergedCellInfo } from '../utils'
|
||||
import { findMergedCellInfo, isInAnySelection } from '../utils'
|
||||
import { Cell } from './Cell'
|
||||
|
||||
// Рендер-проп для ячеек грида
|
||||
@@ -13,6 +13,7 @@ export const CellRenderer = ({
|
||||
sheetType,
|
||||
widths,
|
||||
selectedCell,
|
||||
multiSelection,
|
||||
activeSheet,
|
||||
isEditing,
|
||||
editingValue,
|
||||
@@ -23,6 +24,9 @@ export const CellRenderer = ({
|
||||
handleCellDoubleClick,
|
||||
handleCellChange,
|
||||
handleCellReferenceClick,
|
||||
handleCellMouseDown,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseUp,
|
||||
isInEditMode,
|
||||
stopEditing,
|
||||
setActiveSheet,
|
||||
@@ -33,6 +37,40 @@ export const CellRenderer = ({
|
||||
selectedCell?.row === rowIndex &&
|
||||
selectedCell?.col === columnIndex &&
|
||||
activeSheet === sheetType
|
||||
|
||||
// Проверяем, является ли ячейка частью множественного выделения
|
||||
const isInMultiSelection =
|
||||
sheetType === activeSheet &&
|
||||
isInAnySelection(rowIndex, columnIndex, multiSelection)
|
||||
|
||||
const multiSelectionActive =
|
||||
sheetType === activeSheet && multiSelection.length > 0
|
||||
|
||||
// Вычисляем границы выделения, если ячейка внутри выделения
|
||||
let selectionTop = false,
|
||||
selectionBottom = false,
|
||||
selectionLeft = false,
|
||||
selectionRight = false
|
||||
|
||||
if (isInMultiSelection && multiSelection.length > 0) {
|
||||
const bounds = {
|
||||
top: Math.min(...multiSelection.map(s => Math.min(s.startRow, s.endRow))),
|
||||
bottom: Math.max(
|
||||
...multiSelection.map(s => Math.max(s.startRow, s.endRow))
|
||||
),
|
||||
left: Math.min(
|
||||
...multiSelection.map(s => Math.min(s.startCol, s.endCol))
|
||||
),
|
||||
right: Math.max(
|
||||
...multiSelection.map(s => Math.max(s.startCol, s.endCol))
|
||||
),
|
||||
}
|
||||
selectionTop = rowIndex === bounds.top
|
||||
selectionBottom = rowIndex === bounds.bottom
|
||||
selectionLeft = columnIndex === bounds.left
|
||||
selectionRight = columnIndex === bounds.right
|
||||
}
|
||||
|
||||
const isCellEditing = isCellSelected && isEditing
|
||||
|
||||
const cellKey = `${rowIndex}-${columnIndex}`
|
||||
@@ -92,6 +130,12 @@ export const CellRenderer = ({
|
||||
columnWidth={widths[columnIndex]}
|
||||
columnWidths={widths}
|
||||
isSelected={isCellSelected}
|
||||
isInMultiSelection={isInMultiSelection}
|
||||
multiSelectionActive={multiSelectionActive}
|
||||
selectionTop={selectionTop}
|
||||
selectionBottom={selectionBottom}
|
||||
selectionLeft={selectionLeft}
|
||||
selectionRight={selectionRight}
|
||||
isEditing={isCellEditing}
|
||||
displayValue={displayValue}
|
||||
rawValue={rawValue}
|
||||
@@ -102,6 +146,9 @@ export const CellRenderer = ({
|
||||
onCellDoubleClick={handleCellDoubleClick}
|
||||
onCellValueChange={handleCellChange}
|
||||
onCellReferenceClick={handleCellReferenceClick}
|
||||
onCellMouseDown={handleCellMouseDown}
|
||||
onCellMouseEnter={handleCellMouseEnter}
|
||||
onCellMouseUp={handleCellMouseUp}
|
||||
isInEditMode={isEditing}
|
||||
stopEditing={stopEditing}
|
||||
setActiveSheet={setActiveSheet}
|
||||
|
||||
@@ -11,6 +11,7 @@ export const Spreadsheet = ({
|
||||
spreadsheetWidths,
|
||||
getCachedCellData,
|
||||
selectedCell,
|
||||
multiSelection,
|
||||
activeSheet,
|
||||
isEditing,
|
||||
editingValue,
|
||||
@@ -19,6 +20,9 @@ export const Spreadsheet = ({
|
||||
handleCellDoubleClick,
|
||||
handleCellChange,
|
||||
handleCellReferenceClick,
|
||||
handleCellMouseDown,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseUp,
|
||||
isInEditMode,
|
||||
stopEditing,
|
||||
setActiveSheet,
|
||||
@@ -39,16 +43,22 @@ export const Spreadsheet = ({
|
||||
sheetType,
|
||||
widths,
|
||||
selectedCell,
|
||||
multiSelection,
|
||||
activeSheet,
|
||||
isEditing,
|
||||
editingValue,
|
||||
cachedData,
|
||||
mergedCells,
|
||||
highlightedCells,
|
||||
multiSelectionActive:
|
||||
activeSheet === sheetType && multiSelection.length > 0,
|
||||
handleCellClick,
|
||||
handleCellDoubleClick,
|
||||
handleCellChange,
|
||||
handleCellReferenceClick,
|
||||
handleCellMouseDown,
|
||||
handleCellMouseEnter,
|
||||
handleCellMouseUp,
|
||||
isInEditMode,
|
||||
stopEditing,
|
||||
setActiveSheet,
|
||||
@@ -67,6 +77,9 @@ export const Spreadsheet = ({
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
const [gridSize, setGridSize] = useState({ width: 0, height: 0 })
|
||||
|
||||
// Состояние текущего скролла грида
|
||||
const [scrollPos, setScrollPos] = useState({ left: 0, top: 0 })
|
||||
|
||||
useEffect(() => {
|
||||
const parent = parentRef.current
|
||||
if (!parent) return
|
||||
@@ -82,13 +95,25 @@ export const Spreadsheet = ({
|
||||
return () => resizeObserver.disconnect()
|
||||
}, [])
|
||||
|
||||
const selectedRows = new Set<number>()
|
||||
const selectedCols = new Set<number>()
|
||||
if (sheetType === activeSheet && multiSelection.length > 0) {
|
||||
multiSelection.forEach(sel => {
|
||||
const top = Math.min(sel.startRow, sel.endRow)
|
||||
const bottom = Math.max(sel.startRow, sel.endRow)
|
||||
const left = Math.min(sel.startCol, sel.endCol)
|
||||
const right = Math.max(sel.startCol, sel.endCol)
|
||||
for (let r = top; r <= bottom; r++) selectedRows.add(r)
|
||||
for (let c = left; c <= right; c++) selectedCols.add(c)
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={parentRef}
|
||||
className="flex min-w-0 flex-col overflow-hidden"
|
||||
style={{
|
||||
flexBasis: `${spreadsheetWidths[SHEET_NAMES[sheetType]] * 100}%`,
|
||||
transition: 'none',
|
||||
}}
|
||||
onClick={() => setActiveSheet(sheetType)}
|
||||
>
|
||||
@@ -98,6 +123,7 @@ export const Spreadsheet = ({
|
||||
}}
|
||||
className="flex h-full flex-col overflow-hidden border bg-card shadow-sm"
|
||||
onKeyDown={handleKeyDown}
|
||||
style={{ userSelect: isEditing ? 'text' : 'none' }}
|
||||
tabIndex={0}
|
||||
>
|
||||
{/* Spreadsheet Body */}
|
||||
@@ -128,7 +154,9 @@ export const Spreadsheet = ({
|
||||
<div>
|
||||
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => {
|
||||
const isActiveRow =
|
||||
selectedCell?.row === rowIndex && activeSheet === sheetType
|
||||
(selectedCell?.row === rowIndex &&
|
||||
activeSheet === sheetType) ||
|
||||
selectedRows.has(rowIndex)
|
||||
return (
|
||||
<div
|
||||
key={rowIndex}
|
||||
@@ -153,7 +181,8 @@ export const Spreadsheet = ({
|
||||
<div className="flex">
|
||||
{Array.from({ length: COL_COUNT }).map((_, i) => {
|
||||
const isActiveColumn =
|
||||
selectedCell?.col === i && activeSheet === sheetType
|
||||
(selectedCell?.col === i && activeSheet === sheetType) ||
|
||||
selectedCols.has(i)
|
||||
const handleMouseDown = (e: React.MouseEvent) => {
|
||||
e.preventDefault()
|
||||
const startX = e.clientX
|
||||
@@ -190,7 +219,61 @@ export const Spreadsheet = ({
|
||||
</div>
|
||||
|
||||
{/* Grid */}
|
||||
<div className="min-h-0 flex-1">
|
||||
<div className="relative min-h-0 flex-1">
|
||||
{/* selection overlay */}
|
||||
{sheetType === activeSheet &&
|
||||
multiSelection.length > 0 &&
|
||||
(() => {
|
||||
const bounds = {
|
||||
topRow: Math.min(
|
||||
...multiSelection.map(sel =>
|
||||
Math.min(sel.startRow, sel.endRow)
|
||||
)
|
||||
),
|
||||
bottomRow: Math.max(
|
||||
...multiSelection.map(sel =>
|
||||
Math.max(sel.startRow, sel.endRow)
|
||||
)
|
||||
),
|
||||
leftCol: Math.min(
|
||||
...multiSelection.map(sel =>
|
||||
Math.min(sel.startCol, sel.endCol)
|
||||
)
|
||||
),
|
||||
rightCol: Math.max(
|
||||
...multiSelection.map(sel =>
|
||||
Math.max(sel.startCol, sel.endCol)
|
||||
)
|
||||
),
|
||||
}
|
||||
|
||||
const topPx =
|
||||
bounds.topRow * 28 /* ROW_HEIGHT */ - scrollPos.top
|
||||
const heightPx = (bounds.bottomRow - bounds.topRow + 1) * 28
|
||||
const leftPx =
|
||||
widths.slice(0, bounds.leftCol).reduce((a, b) => a + b, 0) -
|
||||
scrollPos.left
|
||||
const widthPx = widths
|
||||
.slice(bounds.leftCol, bounds.rightCol + 1)
|
||||
.reduce((a, b) => a + b, 0)
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: topPx,
|
||||
left: leftPx,
|
||||
width: widthPx,
|
||||
height: heightPx,
|
||||
border: '2px solid hsl(var(--primary))',
|
||||
pointerEvents: 'none',
|
||||
boxSizing: 'border-box',
|
||||
transition: 'none',
|
||||
zIndex: 5,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
})()}
|
||||
<VariableSizeGrid
|
||||
ref={(el: VariableSizeGrid | null) => {
|
||||
gridRefs.current[sheetType] = el
|
||||
@@ -204,7 +287,10 @@ export const Spreadsheet = ({
|
||||
itemData={itemData}
|
||||
overscanColumnCount={20}
|
||||
overscanRowCount={10}
|
||||
onScroll={e => onGridScroll(sheetType, e)}
|
||||
onScroll={e => {
|
||||
setScrollPos({ left: e.scrollLeft, top: e.scrollTop })
|
||||
onGridScroll(sheetType, e)
|
||||
}}
|
||||
>
|
||||
{CellRenderer}
|
||||
</VariableSizeGrid>
|
||||
|
||||
@@ -24,6 +24,20 @@ export interface HighlightedCell {
|
||||
color: string // класс CSS для цвета подсветки
|
||||
}
|
||||
|
||||
// Интерфейс для области выделения
|
||||
export interface CellSelection {
|
||||
startRow: number
|
||||
startCol: number
|
||||
endRow: number
|
||||
endCol: number
|
||||
}
|
||||
|
||||
// Интерфейс для выделения множества ячеек
|
||||
export interface MultiSelection {
|
||||
sheet: SheetType
|
||||
selections: CellSelection[]
|
||||
}
|
||||
|
||||
// Кэшированные данные ячеек для оптимизации
|
||||
export interface CachedCellData {
|
||||
displayValue: string
|
||||
@@ -48,13 +62,20 @@ export interface CellProps {
|
||||
columnWidth: number
|
||||
columnWidths: number[] // Добавляем массив всех ширин колонок
|
||||
isSelected: boolean
|
||||
isInMultiSelection: boolean // Является ли ячейка частью множественного выделения
|
||||
multiSelectionActive: boolean // Есть ли вообще активное множественное выделение
|
||||
// Флаги, является ли ячейка на краю выделения
|
||||
selectionTop?: boolean
|
||||
selectionBottom?: boolean
|
||||
selectionLeft?: boolean
|
||||
selectionRight?: boolean
|
||||
isEditing: boolean
|
||||
displayValue: string // Отображаемое значение (вычисленное)
|
||||
rawValue: string // "Сырое" значение (формула)
|
||||
isModified: boolean
|
||||
nextContentCol: number // индекс ближайшей занятой колонки
|
||||
availablePx: number // суммарная ширина, куда можно расширяться
|
||||
onCellClick: (row: number, col: number) => void
|
||||
onCellClick: (row: number, col: number, event?: React.MouseEvent) => void
|
||||
onCellDoubleClick: (row: number, col: number) => void
|
||||
onCellValueChange: (newValue: string) => void // Изменение значения во время редактирования
|
||||
onCellReferenceClick?: (
|
||||
@@ -62,6 +83,9 @@ export interface CellProps {
|
||||
row: number,
|
||||
col: number
|
||||
) => void // Клик для вставки ссылки
|
||||
onCellMouseDown?: (row: number, col: number, event: React.MouseEvent) => void // Для начала drag selection
|
||||
onCellMouseEnter?: (row: number, col: number) => void // Для drag selection
|
||||
onCellMouseUp?: () => void // Для завершения drag selection
|
||||
isInEditMode: boolean // Глобальный режим редактирования
|
||||
stopEditing: (save: boolean) => void
|
||||
setActiveSheet: (type: SheetType) => void
|
||||
@@ -102,13 +126,15 @@ export interface CellRendererData {
|
||||
sheetType: SheetType
|
||||
widths: number[]
|
||||
selectedCell: { row: number; col: number } | null
|
||||
multiSelection: CellSelection[]
|
||||
activeSheet: SheetType
|
||||
isEditing: boolean
|
||||
editingValue: string
|
||||
cachedData: Record<string, CachedCellData>
|
||||
mergedCells?: MergedCell[]
|
||||
highlightedCells: HighlightedCell[] // Добавляем подсвеченные ячейки
|
||||
handleCellClick: (row: number, col: number) => void
|
||||
multiSelectionActive: boolean
|
||||
handleCellClick: (row: number, col: number, event?: React.MouseEvent) => void
|
||||
handleCellDoubleClick: (row: number, col: number) => void
|
||||
handleCellChange: (newValue: string) => void
|
||||
handleCellReferenceClick?: (
|
||||
@@ -116,6 +142,13 @@ export interface CellRendererData {
|
||||
row: number,
|
||||
col: number
|
||||
) => void // Клик для вставки ссылки
|
||||
handleCellMouseDown?: (
|
||||
row: number,
|
||||
col: number,
|
||||
event: React.MouseEvent
|
||||
) => void
|
||||
handleCellMouseEnter?: (row: number, col: number) => void
|
||||
handleCellMouseUp?: () => void
|
||||
isInEditMode: boolean // Глобальный режим редактирования
|
||||
stopEditing: (save: boolean) => void
|
||||
setActiveSheet: (type: SheetType) => void
|
||||
@@ -135,11 +168,12 @@ export interface SpreadsheetProps {
|
||||
spreadsheetWidths: { L: number; R: number }
|
||||
getCachedCellData: (sheetType: SheetType) => Record<string, CachedCellData>
|
||||
selectedCell: { row: number; col: number } | null
|
||||
multiSelection: CellSelection[]
|
||||
activeSheet: SheetType
|
||||
isEditing: boolean
|
||||
editingValue: string
|
||||
highlightedCells: HighlightedCell[] // Добавляем подсвеченные ячейки
|
||||
handleCellClick: (row: number, col: number) => void
|
||||
handleCellClick: (row: number, col: number, event?: React.MouseEvent) => void
|
||||
handleCellDoubleClick: (row: number, col: number) => void
|
||||
handleCellChange: (newValue: string) => void
|
||||
handleCellReferenceClick?: (
|
||||
@@ -147,6 +181,13 @@ export interface SpreadsheetProps {
|
||||
row: number,
|
||||
col: number
|
||||
) => void // Клик для вставки ссылки
|
||||
handleCellMouseDown?: (
|
||||
row: number,
|
||||
col: number,
|
||||
event: React.MouseEvent
|
||||
) => void
|
||||
handleCellMouseEnter?: (row: number, col: number) => void
|
||||
handleCellMouseUp?: () => void
|
||||
isInEditMode: boolean // Глобальный режим редактирования
|
||||
stopEditing: (save: boolean) => void
|
||||
setActiveSheet: (type: SheetType) => void
|
||||
|
||||
@@ -303,3 +303,69 @@ export function convertReferencesToHighlights(
|
||||
|
||||
return highlights
|
||||
}
|
||||
|
||||
// Функции для работы с выделением множества ячеек
|
||||
import { CellSelection } from './types'
|
||||
|
||||
export const isInSelection = (
|
||||
row: number,
|
||||
col: number,
|
||||
selection: CellSelection
|
||||
): boolean => {
|
||||
const { startRow, startCol, endRow, endCol } = selection
|
||||
const minRow = Math.min(startRow, endRow)
|
||||
const maxRow = Math.max(startRow, endRow)
|
||||
const minCol = Math.min(startCol, endCol)
|
||||
const maxCol = Math.max(startCol, endCol)
|
||||
|
||||
return row >= minRow && row <= maxRow && col >= minCol && col <= maxCol
|
||||
}
|
||||
|
||||
export const isInAnySelection = (
|
||||
row: number,
|
||||
col: number,
|
||||
selections: CellSelection[]
|
||||
): boolean => {
|
||||
return selections.some(selection => isInSelection(row, col, selection))
|
||||
}
|
||||
|
||||
export const normalizeSelection = (selection: CellSelection): CellSelection => {
|
||||
const { startRow, startCol, endRow, endCol } = selection
|
||||
return {
|
||||
startRow: Math.min(startRow, endRow),
|
||||
startCol: Math.min(startCol, endCol),
|
||||
endRow: Math.max(startRow, endRow),
|
||||
endCol: Math.max(startCol, endCol),
|
||||
}
|
||||
}
|
||||
|
||||
export const getAllCellsInSelections = (
|
||||
selections: CellSelection[]
|
||||
): Array<{ row: number; col: number }> => {
|
||||
const cells: Array<{ row: number; col: number }> = []
|
||||
|
||||
selections.forEach(selection => {
|
||||
const normalized = normalizeSelection(selection)
|
||||
for (let row = normalized.startRow; row <= normalized.endRow; row++) {
|
||||
for (let col = normalized.startCol; col <= normalized.endCol; col++) {
|
||||
cells.push({ row, col })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return cells
|
||||
}
|
||||
|
||||
export const createSelectionFromRange = (
|
||||
startRow: number,
|
||||
startCol: number,
|
||||
endRow: number,
|
||||
endCol: number
|
||||
): CellSelection => {
|
||||
return {
|
||||
startRow,
|
||||
startCol,
|
||||
endRow,
|
||||
endCol,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user