fix(grid): improve long text cell hover editor
This commit is contained in:
1 file changed
+75
-9
@@ -245,7 +245,7 @@ import {
|
|||||||
type DataGridScrollPosition,
|
type DataGridScrollPosition,
|
||||||
} from "@/lib/dataGrid/dataGridInfiniteScroll";
|
} from "@/lib/dataGrid/dataGridInfiniteScroll";
|
||||||
import { resolveDataGridWheelScroll } from "@/lib/dataGrid/dataGridWheel";
|
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 { 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 { DATA_GRID_DARK_STRIPED_ROW_BG, DATA_GRID_LIGHT_STRIPED_ROW_BG, dataGridActiveRowBackground } from "@/lib/dataGrid/dataGridPaintTheme";
|
||||||
import { createRowLowerTextCache } from "@/lib/dataGrid/dataGridRowLowerText";
|
import { createRowLowerTextCache } from "@/lib/dataGrid/dataGridRowLowerText";
|
||||||
@@ -3451,6 +3451,7 @@ function onScrollerScroll(e: Event) {
|
|||||||
} else {
|
} else {
|
||||||
syncHeaderScroll(e);
|
syncHeaderScroll(e);
|
||||||
}
|
}
|
||||||
|
if (editingCell.value) scheduleActiveCellEditTextareaResize();
|
||||||
markGridScrolling();
|
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) {
|
async function startDomCellEdit(rowId: number, columnIndex: number, displayText: string, event: MouseEvent) {
|
||||||
|
const target = event.currentTarget;
|
||||||
if (!(await hydrateLargeValueCell(rowId, columnIndex))) return;
|
if (!(await hydrateLargeValueCell(rowId, columnIndex))) return;
|
||||||
const item = getRowItem(rowId);
|
const item = getRowItem(rowId);
|
||||||
const editText = item ? cellEditorTextForValue(item.data[columnIndex], columnIndex) : displayText;
|
const editText = item ? cellEditorTextForValue(item.data[columnIndex], columnIndex) : displayText;
|
||||||
@@ -4429,7 +4431,7 @@ async function startDomCellEdit(rowId: number, columnIndex: number, displayText:
|
|||||||
cellEditContentNeedsExpandedEditor({
|
cellEditContentNeedsExpandedEditor({
|
||||||
displayText,
|
displayText,
|
||||||
editText,
|
editText,
|
||||||
target: event.currentTarget,
|
target,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -7409,6 +7411,7 @@ function onCanvasScroll(event: Event) {
|
|||||||
}
|
}
|
||||||
if (headerRef.value && headerRef.value.scrollLeft !== scrollLeft) headerRef.value.scrollLeft = scrollLeft;
|
if (headerRef.value && headerRef.value.scrollLeft !== scrollLeft) headerRef.value.scrollLeft = scrollLeft;
|
||||||
recordScrollPosition({ top: scrollTop, left: scrollLeft });
|
recordScrollPosition({ top: scrollTop, left: scrollLeft });
|
||||||
|
if (editingCell.value) scheduleActiveCellEditTextareaResize();
|
||||||
markGridScrolling();
|
markGridScrolling();
|
||||||
scheduleCanvasDraw();
|
scheduleCanvasDraw();
|
||||||
}
|
}
|
||||||
@@ -7643,8 +7646,29 @@ function canvasCellContentOverflows(item: RowItem, actualColIdx: number, visible
|
|||||||
const displayText = formatCellCached(item.data[actualColIdx], actualColIdx);
|
const displayText = formatCellCached(item.data[actualColIdx], actualColIdx);
|
||||||
const editText = cellEditorTextForValue(item.data[actualColIdx], actualColIdx);
|
const editText = cellEditorTextForValue(item.data[actualColIdx], actualColIdx);
|
||||||
if (editText.includes("\n") || editText.includes("\r") || editText.length > displayText.length) return true;
|
if (editText.includes("\n") || editText.includes("\r") || editText.length > displayText.length) return true;
|
||||||
const textWidth = measureCellTextWidth(displayText, `400 ${tableFontSize.value}px ${tableFontFamily.value}`);
|
const canvas = activeCanvasSurface();
|
||||||
return textWidth > Math.max(0, cellWidth - 24);
|
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) {
|
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) {
|
async function onTransposeCellDblClick(rowIndex: number, actualColIdx: number, displayText: string, event: MouseEvent) {
|
||||||
|
const target = event.currentTarget;
|
||||||
const item = displayItemAt(rowIndex);
|
const item = displayItemAt(rowIndex);
|
||||||
if (!item) return;
|
if (!item) return;
|
||||||
if (!canEditCellItem(item, actualColIdx)) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
await startDomCellEdit(item.id, actualColIdx, displayText, event);
|
await startDomCellEdit(item.id, actualColIdx, displayText, event);
|
||||||
@@ -10490,19 +10515,45 @@ function onCellEditTextareaPaste(event: ClipboardEvent) {
|
|||||||
|
|
||||||
function resizeCellEditTextareaElement(textarea: HTMLTextAreaElement | null) {
|
function resizeCellEditTextareaElement(textarea: HTMLTextAreaElement | null) {
|
||||||
if (!textarea) return;
|
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 visibleBottom = cellEditVisibleBottom(textarea);
|
||||||
const availableHeight = Math.max(36, Math.floor(visibleBottom - textareaRect.top - 10));
|
const visibleTop = cellEditVisibleTop(textarea);
|
||||||
const metrics = cellEditTextMetrics(textarea);
|
const metrics = cellEditTextMetrics(textarea);
|
||||||
const maxVisibleHeight = Math.ceil(metrics.lineHeight * 9.5 + metrics.verticalChrome);
|
const maxVisibleHeight = Math.ceil(metrics.lineHeight * 9.5 + metrics.verticalChrome);
|
||||||
const maxHeight = Math.min(maxVisibleHeight, availableHeight);
|
|
||||||
const naturalHeight = cellEditNaturalHeight(textarea, metrics);
|
const naturalHeight = cellEditNaturalHeight(textarea, metrics);
|
||||||
const targetMinHeight = Math.max(64, Math.min(naturalHeight, 120));
|
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 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-min-height", `${minHeight}px`);
|
||||||
textarea.style.setProperty("--cell-edit-max-height", `${maxHeight}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 = "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) {
|
if (resetCellEditTextareaScrollOnResize) {
|
||||||
textarea.scrollTop = 0;
|
textarea.scrollTop = 0;
|
||||||
textarea.setSelectionRange?.(0, 0);
|
textarea.setSelectionRange?.(0, 0);
|
||||||
@@ -10550,6 +10601,16 @@ function cellEditVisibleBottom(textarea: HTMLTextAreaElement): number {
|
|||||||
return Math.min(...bottoms.filter((bottom) => Number.isFinite(bottom)));
|
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) {
|
function scheduleCellEditTextareaResize(textarea: HTMLTextAreaElement | null) {
|
||||||
if (!textarea || typeof requestAnimationFrame !== "function") return;
|
if (!textarea || typeof requestAnimationFrame !== "function") return;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
@@ -12731,6 +12792,7 @@ function openGridSnapshot() {
|
|||||||
@input="onCellEditTextareaInput"
|
@input="onCellEditTextareaInput"
|
||||||
@keydown.stop="onCellEditKeydown"
|
@keydown.stop="onCellEditKeydown"
|
||||||
@paste.stop="onCellEditTextareaPaste"
|
@paste.stop="onCellEditTextareaPaste"
|
||||||
|
@wheel.stop
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
@@ -13473,6 +13535,7 @@ function openGridSnapshot() {
|
|||||||
@input="onCellEditTextareaInput"
|
@input="onCellEditTextareaInput"
|
||||||
@keydown.stop="onCellEditKeydown"
|
@keydown.stop="onCellEditKeydown"
|
||||||
@paste.stop="onCellEditTextareaPaste"
|
@paste.stop="onCellEditTextareaPaste"
|
||||||
|
@wheel.stop
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
@@ -13682,6 +13745,7 @@ function openGridSnapshot() {
|
|||||||
@input="onCellEditTextareaInput"
|
@input="onCellEditTextareaInput"
|
||||||
@keydown.stop="onCellEditKeydown"
|
@keydown.stop="onCellEditKeydown"
|
||||||
@paste.stop="onCellEditTextareaPaste"
|
@paste.stop="onCellEditTextareaPaste"
|
||||||
|
@wheel.stop
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
v-else
|
v-else
|
||||||
@@ -15008,11 +15072,13 @@ function openGridSnapshot() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cell-edit-input--expanded {
|
.cell-edit-input--expanded {
|
||||||
|
position: fixed;
|
||||||
left: 7px;
|
left: 7px;
|
||||||
width: calc(100% - 14px);
|
width: calc(100% - 14px);
|
||||||
min-height: var(--cell-edit-min-height, 54px);
|
min-height: var(--cell-edit-min-height, 54px);
|
||||||
max-height: var(--cell-edit-max-height, calc(9.5lh + 10px));
|
max-height: var(--cell-edit-max-height, calc(9.5lh + 10px));
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: color-mix(in oklab, var(--foreground) 24%, transparent) transparent;
|
scrollbar-color: color-mix(in oklab, var(--foreground) 24%, transparent) transparent;
|
||||||
resize: none;
|
resize: none;
|
||||||
|
|||||||
Reference in new issue
Block a user