fix(grid): improve long text cell hover editor

This commit is contained in:
zipg authored and GitHub committed 2026-08-31 16:59:20 +08:00
1 parent d144e1bc08
commit 0812e57cf1
1 file changed
+75 -9
+75 -9
View File
@@ -245,7 +245,7 @@ import {
type DataGridScrollPosition,
} from "@/lib/dataGrid/dataGridInfiniteScroll";
import { resolveDataGridWheelScroll } from "@/lib/dataGrid/dataGridWheel";
import { CANVAS_DATA_GRID_ROW_HEIGHT, MAX_CANVAS_DATA_GRID_PIXEL_RATIO, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer";
import { CANVAS_DATA_GRID_ROW_HEIGHT, MAX_CANVAS_DATA_GRID_PIXEL_RATIO, canvasDataGridActionOverlayWidth, canvasDataGridActionReservedWidth, dataGridSearchMatchKey, drawCanvasDataGrid, resolveCanvasCellTextLayout, type CanvasDevicePixelSize } from "@/lib/dataGrid/canvasDataGridRenderer";
import { resolveCrosshairTarget, type CrosshairTarget } from "@/lib/dataGrid/crosshairHighlight";
import { DATA_GRID_DARK_STRIPED_ROW_BG, DATA_GRID_LIGHT_STRIPED_ROW_BG, dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme";
import { createRowLowerTextCache } from "@/lib/dataGrid/dataGridRowLowerText";
@@ -3451,6 +3451,7 @@ function onScrollerScroll(e: Event) {
} else {
syncHeaderScroll(e);
}
if (editingCell.value) scheduleActiveCellEditTextareaResize();
markGridScrolling();
}
@@ -4420,6 +4421,7 @@ async function startCellEdit(rowId: number, columnIndex: number, expanded: boole
}
async function startDomCellEdit(rowId: number, columnIndex: number, displayText: string, event: MouseEvent) {
const target = event.currentTarget;
if (!(await hydrateLargeValueCell(rowId, columnIndex))) return;
const item = getRowItem(rowId);
const editText = item ? cellEditorTextForValue(item.data[columnIndex], columnIndex) : displayText;
@@ -4429,7 +4431,7 @@ async function startDomCellEdit(rowId: number, columnIndex: number, displayText:
cellEditContentNeedsExpandedEditor({
displayText,
editText,
target: event.currentTarget,
target,
}),
);
}
@@ -7409,6 +7411,7 @@ function onCanvasScroll(event: Event) {
}
if (headerRef.value && headerRef.value.scrollLeft !== scrollLeft) headerRef.value.scrollLeft = scrollLeft;
recordScrollPosition({ top: scrollTop, left: scrollLeft });
if (editingCell.value) scheduleActiveCellEditTextareaResize();
markGridScrolling();
scheduleCanvasDraw();
}
@@ -7643,8 +7646,29 @@ function canvasCellContentOverflows(item: RowItem, actualColIdx: number, visible
const displayText = formatCellCached(item.data[actualColIdx], actualColIdx);
const editText = cellEditorTextForValue(item.data[actualColIdx], actualColIdx);
if (editText.includes("\n") || editText.includes("\r") || editText.length > displayText.length) return true;
const textWidth = measureCellTextWidth(displayText, `400 ${tableFontSize.value}px ${tableFontFamily.value}`);
return textWidth > Math.max(0, cellWidth - 24);
const canvas = activeCanvasSurface();
const context = canvas?.getContext("2d");
if (!context) {
const textWidth = measureCellTextWidth(displayText, `400 ${tableFontSize.value}px ${tableFontFamily.value}`);
return textWidth > Math.max(0, cellWidth - 24);
}
const isRightAlign = columnAligns.value[visibleColIdx] === "right";
const actionCell = canvasRightAlignedActionCell.value;
const reservedWidth = actionCell?.rowIndex === item.displayIndex && actionCell.visibleColIdx === visibleColIdx ? actionCell.reservedWidth : 0;
const { maxWidth } = resolveCanvasCellTextLayout({
drawX: 0,
colWidth: cellWidth,
dpr: 1,
isRightAlign,
reservedWidth,
});
context.save();
context.font = `400 ${tableFontSize.value}px ${tableFontFamily.value}`;
if ("fontVariantNumeric" in context) (context as CanvasRenderingContext2D & { fontVariantNumeric?: string }).fontVariantNumeric = "tabular-nums";
const textWidth = context.measureText(displayText).width;
context.restore();
return textWidth > maxWidth;
}
function canvasCellViewportRect(rowIndex: number, visibleColIdx: number) {
@@ -8511,10 +8535,11 @@ function showTransposeCellDetails(rowIndex: number, actualColIdx: number) {
}
async function onTransposeCellDblClick(rowIndex: number, actualColIdx: number, displayText: string, event: MouseEvent) {
const target = event.currentTarget;
const item = displayItemAt(rowIndex);
if (!item) return;
if (!canEditCellItem(item, actualColIdx)) {
await startReadonlyCellTextSelection(item.id, actualColIdx, displayText, cellEditContentNeedsExpandedEditor({ displayText, editText: cellEditorTextForValue(item.data[actualColIdx], actualColIdx), target: event.currentTarget }));
await startReadonlyCellTextSelection(item.id, actualColIdx, displayText, cellEditContentNeedsExpandedEditor({ displayText, editText: cellEditorTextForValue(item.data[actualColIdx], actualColIdx), target }));
return;
}
await startDomCellEdit(item.id, actualColIdx, displayText, event);
@@ -10490,19 +10515,45 @@ function onCellEditTextareaPaste(event: ClipboardEvent) {
function resizeCellEditTextareaElement(textarea: HTMLTextAreaElement | null) {
if (!textarea) return;
const textareaRect = textarea.getBoundingClientRect();
// The result grid is a scroll container, so an absolutely positioned editor
// cannot escape its bottom clipping edge. Use the cell's viewport rect as the
// anchor and position the editor itself in the viewport instead.
const anchorRect = textarea.parentElement?.getBoundingClientRect();
if (!anchorRect) return;
const visibleBottom = cellEditVisibleBottom(textarea);
const availableHeight = Math.max(36, Math.floor(visibleBottom - textareaRect.top - 10));
const visibleTop = cellEditVisibleTop(textarea);
const metrics = cellEditTextMetrics(textarea);
const maxVisibleHeight = Math.ceil(metrics.lineHeight * 9.5 + metrics.verticalChrome);
const maxHeight = Math.min(maxVisibleHeight, availableHeight);
const naturalHeight = cellEditNaturalHeight(textarea, metrics);
const targetMinHeight = Math.max(64, Math.min(naturalHeight, 120));
const preferredHeight = Math.min(maxVisibleHeight, Math.max(targetMinHeight, naturalHeight));
const spaceBelow = Math.max(0, Math.floor(visibleBottom - anchorRect.top - 10));
const spaceAbove = Math.max(0, Math.floor(anchorRect.bottom - visibleTop - 10));
const placeAbove = spaceBelow < preferredHeight && spaceAbove > spaceBelow;
const availableHeight = placeAbove ? spaceAbove : spaceBelow;
const maxHeight = Math.min(maxVisibleHeight, Math.max(36, availableHeight));
const minHeight = Math.min(targetMinHeight, maxHeight);
const height = Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight));
const top = placeAbove ? Math.max(visibleTop, Math.min(anchorRect.top, visibleBottom - height)) : anchorRect.top;
const horizontalInset = 7;
const desiredWidth = Math.max(0, anchorRect.width - horizontalInset * 2);
const maxViewportWidth = Math.max(0, window.innerWidth - horizontalInset * 2);
const width = Math.min(desiredWidth, maxViewportWidth);
const left = Math.min(Math.max(horizontalInset, anchorRect.left + horizontalInset), Math.max(horizontalInset, window.innerWidth - width - horizontalInset));
textarea.style.setProperty("--cell-edit-min-height", `${minHeight}px`);
textarea.style.setProperty("--cell-edit-max-height", `${maxHeight}px`);
textarea.style.left = `${left}px`;
textarea.style.width = `${width}px`;
textarea.style.height = "auto";
textarea.style.height = `${Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight))}px`;
textarea.style.height = `${height}px`;
textarea.style.top = `${top}px`;
// Virtualized and contained scrollers can establish a fixed-position
// containing block in either viewport or scrolled-content coordinates. Use
// the rendered rect to correct that implementation detail back to viewport
// coordinates instead of guessing from the ancestor tree.
const positionedRect = textarea.getBoundingClientRect();
textarea.style.left = `${left + (left - positionedRect.left)}px`;
textarea.style.top = `${top + (top - positionedRect.top)}px`;
if (resetCellEditTextareaScrollOnResize) {
textarea.scrollTop = 0;
textarea.setSelectionRange?.(0, 0);
@@ -10550,6 +10601,16 @@ function cellEditVisibleBottom(textarea: HTMLTextAreaElement): number {
return Math.min(...bottoms.filter((bottom) => Number.isFinite(bottom)));
}
function cellEditVisibleTop(textarea: HTMLTextAreaElement): number {
const tops: number[] = [];
const scroller = cellEditScrollerElement(textarea);
const root = gridRef.value;
if (scroller) tops.push(scroller.getBoundingClientRect().top);
if (root) tops.push(root.getBoundingClientRect().top);
if (typeof window !== "undefined") tops.push(0);
return Math.max(...tops.filter((top) => Number.isFinite(top)));
}
function scheduleCellEditTextareaResize(textarea: HTMLTextAreaElement | null) {
if (!textarea || typeof requestAnimationFrame !== "function") return;
requestAnimationFrame(() => {
@@ -12731,6 +12792,7 @@ function openGridSnapshot() {
@input="onCellEditTextareaInput"
@keydown.stop="onCellEditKeydown"
@paste.stop="onCellEditTextareaPaste"
@wheel.stop
/>
<input
v-else
@@ -13473,6 +13535,7 @@ function openGridSnapshot() {
@input="onCellEditTextareaInput"
@keydown.stop="onCellEditKeydown"
@paste.stop="onCellEditTextareaPaste"
@wheel.stop
/>
<input
v-else
@@ -13682,6 +13745,7 @@ function openGridSnapshot() {
@input="onCellEditTextareaInput"
@keydown.stop="onCellEditKeydown"
@paste.stop="onCellEditTextareaPaste"
@wheel.stop
/>
<input
v-else
@@ -15008,11 +15072,13 @@ function openGridSnapshot() {
}
.cell-edit-input--expanded {
position: fixed;
left: 7px;
width: calc(100% - 14px);
min-height: var(--cell-edit-min-height, 54px);
max-height: var(--cell-edit-max-height, calc(9.5lh + 10px));
overflow-y: auto;
overscroll-behavior: contain;
scrollbar-width: thin;
scrollbar-color: color-mix(in oklab, var(--foreground) 24%, transparent) transparent;
resize: none;