выделение ячеек
This commit is contained in:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user