перепрыгивая текста

This commit is contained in:
2025-07-20 19:42:48 +03:00
parent f162bcf1de
commit 5defbed5d3
5 changed files with 56 additions and 29 deletions

View File

@@ -127,7 +127,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
const getCachedCellData = useCallback(
(sheetType: SheetType): Record<string, CachedCellData> => {
const cacheKey = `${sheetType}-${revision}`
const cacheKey = `${sheetType}-${revision}-${JSON.stringify(columnWidths[sheetType])}`
if (cellDataCache.current[cacheKey]) {
return cellDataCache.current[cacheKey]
@@ -147,23 +147,32 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
}
}
const rightContentMap: Record<string, boolean> = {}
// 1. Находим ближайшую занятую колонку справа для каждой ячейки строки
const nextContentInRow: Record<number, number[]> = {}
for (let row = 0; row < ROW_COUNT; row++) {
let hasContentOnRight = false
nextContentInRow[row] = Array(COL_COUNT).fill(COL_COUNT)
let next = COL_COUNT
for (let col = COL_COUNT - 1; col >= 0; col--) {
const cellKey = `${row}-${col}`
rightContentMap[cellKey] = hasContentOnRight
nextContentInRow[row][col] = next // сохранить для использования ниже
if (allCellValues[cellKey]) {
hasContentOnRight = true
next = col // текущая колонка занята → "новый" предел
}
}
}
// 2. Заполняем cache
for (let row = 0; row < ROW_COUNT; row++) {
for (let col = 0; col < COL_COUNT; col++) {
const cellKey = `${row}-${col}`
const rawValue = multiSheetEngine.getCellRawValue(sheetName, row, col)
const displayValue = allCellValues[cellKey]
const nextCol = nextContentInRow[row][col]
// ширина от текущей колонки до nextCol-1 включительно
const availablePx = columnWidths[sheetType]
.slice(col, nextCol)
.reduce((sum, w) => sum + w, 0)
const cellName = getColumnLabel(col) + (row + 1)
const baseFileValue = initialTemplateValues.current[cellName] ?? '' // Базовое значение из файла
@@ -209,7 +218,8 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
displayValue,
rawValue,
isModified,
hasRightContent: rightContentMap[cellKey],
nextContentCol: nextCol,
availablePx,
}
}
}
@@ -226,7 +236,7 @@ const DualSpreadsheet: FC<DualSpreadsheetProps> = ({
cellDataCache.current[cacheKey] = cachedData
return cachedData
},
[revision, multiSheetEngine, initialTemplateValues]
[revision, multiSheetEngine, initialTemplateValues, columnWidths]
)
const formulaBarValue = useMemo(() => {

View File

@@ -13,7 +13,8 @@ export const Cell = memo(
displayValue,
rawValue,
isModified,
hasRightContent,
nextContentCol,
availablePx,
onCellClick,
onCellDoubleClick,
onCellValueChange,
@@ -28,9 +29,7 @@ export const Cell = memo(
return null
}
const className = `border border-border ${isSelected ? 'bg-accent' : ''} ${
isModified && !mergedCellInfo?.isMerged ? 'bg-yellow-200' : ''
}`
const className = `border border-border ${isModified && !mergedCellInfo?.isMerged ? 'bg-yellow-200' : ''}`
// Отладка стилей для измененных ячеек
if (isModified) {
@@ -108,6 +107,7 @@ export const Cell = memo(
type="text"
value={rawValue} // Во время редактирования показываем текущее редактируемое значение
onChange={e => onCellValueChange(e.target.value)}
style={{ width: columnWidth - 4 }}
onBlur={e => {
// Проверяем, не переходит ли фокус в formula bar
const relatedTarget = e.relatedTarget as HTMLElement
@@ -125,13 +125,14 @@ export const Cell = memo(
stopEditing(false)
}
}}
className="absolute left-0 top-0 z-20 box-border h-full w-full border-2 border-primary px-2 py-1"
className="absolute left-0 top-0 z-20 box-border h-full border-2 border-primary px-2 py-1"
/>
) : (
// Контейнер, который допускает перепрыгивание текста, если впереди есть свободное место
<div
className="relative flex h-full w-full items-center px-2"
style={{
overflow: hasRightContent ? 'hidden' : 'visible',
overflow: availablePx > columnWidth ? 'visible' : 'hidden',
whiteSpace: 'nowrap',
...(isSelected
? { boxShadow: 'inset 0 0 0 2px hsl(var(--primary))' }
@@ -139,12 +140,17 @@ export const Cell = memo(
}}
>
<div
className="relative"
className="absolute left-1 top-0 flex h-full items-center"
style={{
overflow: hasRightContent ? 'hidden' : 'visible',
textOverflow: hasRightContent ? 'ellipsis' : 'clip',
width:
availablePx > columnWidth
? availablePx - 8
: columnWidth - 16,
whiteSpace: 'nowrap',
maxWidth: hasRightContent ? `${columnWidth - 16}px` : 'none',
overflow: 'hidden',
textOverflow: 'ellipsis',
pointerEvents: 'none', // чтобы клики шли в "родную" ячейку
zIndex: 1,
}}
>
{cellDisplayValue}

View File

@@ -82,7 +82,8 @@ export const CellRenderer = ({
displayValue={displayValue}
rawValue={rawValue}
isModified={cellData?.isModified || false}
hasRightContent={cellData?.hasRightContent || false}
nextContentCol={cellData?.nextContentCol || 0}
availablePx={cellData?.availablePx || widths[columnIndex]}
onCellClick={handleCellClick}
onCellDoubleClick={handleCellDoubleClick}
onCellValueChange={handleCellChange}

View File

@@ -107,14 +107,18 @@ export const Spreadsheet = ({
style={{ height: gridSize.height }}
>
<div>
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => (
{Array.from({ length: ROW_COUNT }).map((_, rowIndex) => {
const isActiveRow =
selectedCell?.row === rowIndex && activeSheet === sheetType
return (
<div
key={rowIndex}
className="flex h-7 w-10 items-center justify-center border-b border-r border-border text-xs font-medium text-muted-foreground"
className={`flex h-7 w-10 items-center justify-center border-b border-r border-border text-xs font-medium ${isActiveRow ? 'bg-primary/15 font-medium text-foreground' : 'text-muted-foreground'}`}
>
{rowIndex + 1}
</div>
))}
)
})}
</div>
</div>
</div>
@@ -127,6 +131,8 @@ export const Spreadsheet = ({
>
<div className="flex">
{Array.from({ length: COL_COUNT }).map((_, i) => {
const isActiveColumn =
selectedCell?.col === i && activeSheet === sheetType
const handleMouseDown = (e: React.MouseEvent) => {
e.preventDefault()
const startX = e.clientX
@@ -148,7 +154,7 @@ export const Spreadsheet = ({
return (
<div
key={i}
className="relative flex h-7 flex-shrink-0 items-center justify-center border-b border-r border-border bg-muted text-center text-xs font-medium text-muted-foreground"
className={`relative flex h-7 flex-shrink-0 items-center justify-center border-b border-r border-border bg-muted text-center text-xs font-medium ${isActiveColumn ? 'bg-primary/15 font-medium text-foreground' : 'text-muted-foreground'}`}
style={{ width: widths[i] }}
>
{getColumnLabel(i)}
@@ -175,6 +181,8 @@ export const Spreadsheet = ({
height={gridSize.height}
width={gridSize.width}
itemData={itemData}
overscanColumnCount={20}
overscanRowCount={10}
onScroll={e => onGridScroll(sheetType, e)}
>
{CellRenderer}

View File

@@ -21,7 +21,8 @@ export interface CachedCellData {
displayValue: string
rawValue: string
isModified: boolean
hasRightContent: boolean
nextContentCol: number // индекс ближайшей занятой колонки (COL_COUNT, если нет)
availablePx: number // суммарная ширина, куда можно расширяться
}
export interface DualSpreadsheetProps {
@@ -41,7 +42,8 @@ export interface CellProps {
displayValue: string // Отображаемое значение (вычисленное)
rawValue: string // "Сырое" значение (формула)
isModified: boolean
hasRightContent: boolean // Есть ли содержимое в ячейках справа
nextContentCol: number // индекс ближайшей занятой колонки
availablePx: number // суммарная ширина, куда можно расширяться
onCellClick: (row: number, col: number) => void
onCellDoubleClick: (row: number, col: number) => void
onCellValueChange: (newValue: string) => void // Изменение значения во время редактирования