From ac8be1a3475ffaae2b6a48694ef47cad5c407df0 Mon Sep 17 00:00:00 2001 From: tlartem Date: Tue, 22 Jul 2025 09:32:58 +0300 Subject: [PATCH] =?UTF-8?q?=D0=B2=D1=8B=D0=B4=D0=B5=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B5=20=D1=8F=D1=87=D0=B5=D0=B5=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DualSpreadsheet/DualSpreadsheet.tsx | 168 +++++++++++++++++- .../DualSpreadsheet/components/Cell.tsx | 66 +++++-- .../components/CellRenderer.tsx | 49 ++++- .../components/Spreadsheet.tsx | 96 +++++++++- src/component/DualSpreadsheet/types.ts | 47 ++++- src/component/DualSpreadsheet/utils.ts | 66 +++++++ 6 files changed, 467 insertions(+), 25 deletions(-) diff --git a/src/component/DualSpreadsheet/DualSpreadsheet.tsx b/src/component/DualSpreadsheet/DualSpreadsheet.tsx index 05bbe19..9cca5ec 100644 --- a/src/component/DualSpreadsheet/DualSpreadsheet.tsx +++ b/src/component/DualSpreadsheet/DualSpreadsheet.tsx @@ -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 = ({ row: number col: number } | null>(null) + // Состояние для множественного выделения + const [multiSelection, setMultiSelection] = useState([]) + const [isDragging, setIsDragging] = useState(false) + const [dragStart, setDragStart] = useState<{ + row: number + col: number + } | null>(null) const [isEditing, setIsEditing] = useState(false) const [editingValue, setEditingValue] = useState('') // Локальное значение во время редактирования // Источник ввода: 'cell' (редактирование внутри ячейки) или 'formula' @@ -396,6 +406,8 @@ const DualSpreadsheet: FC = ({ const startEditing = useCallback(() => { if (!selectedCell) return + // Сбрасываем множественное выделение при начале редактирования + setMultiSelection([]) const sheetName = SHEET_NAMES[activeSheet] const rawValue = multiSheetEngine.getCellRawValue( sheetName, @@ -465,6 +477,8 @@ const DualSpreadsheet: FC = ({ ) updateRevision() multiSheetEngine.debouncedRecalc() + // Сбрасываем множественное выделение при очистке одиночной ячейки + setMultiSelection([]) }, [activeSheet, selectedCell, multiSheetEngine, updateRevision]) const moveSelection = useCallback( @@ -488,10 +502,30 @@ const DualSpreadsheet: FC = ({ ) 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 = ({ 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 = ({ 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 = ({ 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 = ({ spreadsheetWidths={spreadsheetWidths} getCachedCellData={getCachedCellData} selectedCell={selectedCell} + multiSelection={multiSelection} activeSheet={activeSheet} isEditing={isEditing} editingValue={editingValue} @@ -948,6 +1103,9 @@ const DualSpreadsheet: FC = ({ handleCellDoubleClick={handleCellDoubleClick} handleCellChange={handleCellChange} handleCellReferenceClick={handleCellReferenceClick} + handleCellMouseDown={handleCellMouseDown} + handleCellMouseEnter={handleCellMouseEnter} + handleCellMouseUp={handleCellMouseUp} isInEditMode={isEditing} stopEditing={stopEditing} setActiveSheet={setActiveSheet} diff --git a/src/component/DualSpreadsheet/components/Cell.tsx b/src/component/DualSpreadsheet/components/Cell.tsx index aaf5e10..998e786 100644 --- a/src/component/DualSpreadsheet/components/Cell.tsx +++ b/src/component/DualSpreadsheet/components/Cell.tsx @@ -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 (
@@ -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' } diff --git a/src/component/DualSpreadsheet/components/CellRenderer.tsx b/src/component/DualSpreadsheet/components/CellRenderer.tsx index 9612bc9..1d95d22 100644 --- a/src/component/DualSpreadsheet/components/CellRenderer.tsx +++ b/src/component/DualSpreadsheet/components/CellRenderer.tsx @@ -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} diff --git a/src/component/DualSpreadsheet/components/Spreadsheet.tsx b/src/component/DualSpreadsheet/components/Spreadsheet.tsx index 2cc76c5..54230cd 100644 --- a/src/component/DualSpreadsheet/components/Spreadsheet.tsx +++ b/src/component/DualSpreadsheet/components/Spreadsheet.tsx @@ -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(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() + const selectedCols = new Set() + 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 (
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 = ({
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => { const isActiveRow = - selectedCell?.row === rowIndex && activeSheet === sheetType + (selectedCell?.row === rowIndex && + activeSheet === sheetType) || + selectedRows.has(rowIndex) return (
{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 = ({
{/* Grid */} -
+
+ {/* 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 ( +
+ ) + })()} { 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} diff --git a/src/component/DualSpreadsheet/types.ts b/src/component/DualSpreadsheet/types.ts index e25bc2f..c16ec7d 100644 --- a/src/component/DualSpreadsheet/types.ts +++ b/src/component/DualSpreadsheet/types.ts @@ -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 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 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 diff --git a/src/component/DualSpreadsheet/utils.ts b/src/component/DualSpreadsheet/utils.ts index 337af01..6cb756e 100644 --- a/src/component/DualSpreadsheet/utils.ts +++ b/src/component/DualSpreadsheet/utils.ts @@ -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, + } +}