выделение ячеек

This commit is contained in:
2025-07-22 09:32:58 +03:00
parent a71f018608
commit ac8be1a347
6 changed files with 467 additions and 25 deletions

View File

@@ -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' }

View File

@@ -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}

View File

@@ -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>