feat(grid): add expanded overflow cell editor

This commit is contained in:
zipg 2026-06-28 02:09:31 +08:00 committed by GitHub
parent 28e7cdfacc
commit a25bd5441f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 355 additions and 13 deletions

View File

@ -323,6 +323,7 @@ const dataGridRenderMode = computed(() => settingsStore.editorSettings.dataGridR
const compactDataGridToolbar = computed(() => dataGridTopbarWidth.value > 0 && dataGridTopbarWidth.value < DATA_GRID_COMPACT_TOPBAR_WIDTH);
const infiniteScrollEnabled = computed(() => settingsStore.editorSettings.infiniteScroll);
const infiniteScrollMaxRows = computed(() => settingsStore.editorSettings.infiniteScrollMaxRows);
const expandedCellEditor = ref<{ rowId: number; col: number } | null>(null);
function headerColumnComment(column: string): string {
if (!showColumnCommentsInHeader.value) return "";
@ -1631,6 +1632,8 @@ const gridVerticalScrollbarDragging = ref(false);
let gridHorizontalScrollbarFrame = 0;
let gridHorizontalScrollbarResizeObserver: ResizeObserver | null = null;
let dataGridTopbarResizeObserver: ResizeObserver | null = null;
let cellEditResizeObserver: ResizeObserver | null = null;
let resetCellEditTextareaScrollOnResize = false;
let gridHorizontalScrollbarDragState: {
trackRect: DOMRect;
thumbOffsetPx: number;
@ -2877,6 +2880,56 @@ function canEditCellItem(item: RowItem | undefined, columnIndex: number): boolea
return true;
}
function cellUsesExpandedEditor(rowId: number | undefined, columnIndex: number): boolean {
return !!expandedCellEditor.value && expandedCellEditor.value.rowId === rowId && expandedCellEditor.value.col === columnIndex;
}
function startCellEdit(rowId: number, columnIndex: number, expanded: boolean) {
expandedCellEditor.value = expanded ? { rowId, col: columnIndex } : null;
startEdit(rowId, columnIndex);
}
function startDomCellEdit(rowId: number, columnIndex: number, displayText: string, event: MouseEvent) {
const item = getRowItem(rowId);
const editText = item ? cellEditorTextForValue(item.data[columnIndex], columnIndex) : displayText;
startCellEdit(rowId, columnIndex, cellEditContentNeedsExpandedEditor({ displayText, editText, target: event.currentTarget }));
}
function cellEditContentNeedsExpandedEditor(options: { displayText: string; editText: string; target: EventTarget | null }): boolean {
const text = options.editText || options.displayText;
if (text.includes("\n") || text.includes("\r")) return true;
if (text.length > options.displayText.length) return true;
return cellTextOverflowsElement(options.displayText, options.target);
}
function cellEditorTextForValue(value: CellValue | undefined, columnIndex: number): string {
return dataGridCellEditorText({
value: value ?? null,
databaseType: props.databaseType,
columnInfo: tableColumnForGridColumn(columnIndex),
});
}
function cellTextOverflowsElement(text: string, target: EventTarget | null): boolean {
if (!(target instanceof HTMLElement)) return false;
const style = window.getComputedStyle(target);
const paddingLeft = Number.parseFloat(style.paddingLeft) || 0;
const paddingRight = Number.parseFloat(style.paddingRight) || 0;
const availableWidth = Math.max(0, target.clientWidth - paddingLeft - paddingRight - 2);
return measureCellTextWidth(text, style.font) > availableWidth;
}
function measureCellTextWidth(text: string, font: string): number {
const canvas = canvasRef.value ?? document.createElement("canvas");
const context = canvas.getContext("2d");
if (!context) return 0;
context.save();
context.font = font;
const width = context.measureText(text).width;
context.restore();
return width;
}
function tableColumnForGridColumn(columnIndex: number): ColumnInfo | undefined {
const columnName = props.sourceColumns?.[columnIndex] ?? props.result.columns[columnIndex];
if (!columnName) return undefined;
@ -2899,6 +2952,25 @@ function isEnumGridColumnNullable(columnIndex: number): boolean {
return tableColumnForGridColumn(columnIndex)?.is_nullable ?? false;
}
function cellEditInputModeForColumn(columnIndex: number): "decimal" | "numeric" | undefined {
const dataType = normalizedColumnDataType(tableColumnForGridColumn(columnIndex));
if (isIntegerColumnType(dataType)) return "numeric";
if (isDecimalColumnType(dataType)) return "decimal";
return undefined;
}
function normalizedColumnDataType(column: ColumnInfo | undefined): string {
return (column?.data_type ?? "").trim().toLowerCase();
}
function isIntegerColumnType(dataType: string): boolean {
return /^(tinyint|smallint|mediumint|int|integer|bigint|serial|smallserial|bigserial|int2|int4|int8|uint|uint8|uint16|uint32|uint64)\b/.test(dataType);
}
function isDecimalColumnType(dataType: string): boolean {
return /^(decimal|numeric|number|float|double|real|money|smallmoney|dec|fixed)\b/.test(dataType);
}
function canDeleteRowItem(item: RowItem | undefined): boolean {
return !!props.editable && !!item && !item.isDeleted && (item.isNew || canEditExistingRows.value);
}
@ -4351,7 +4423,17 @@ function onCanvasDblClick(event: MouseEvent) {
const item = displayItemAt(hit.rowIndex);
const actualColIdx = visibleColumnIndexes.value[hit.visibleColIdx];
if (!item || actualColIdx === undefined || !canEditCellItem(item, actualColIdx)) return;
startEdit(item.id, actualColIdx);
startCellEdit(item.id, actualColIdx, canvasCellContentOverflows(item, actualColIdx, hit.visibleColIdx));
}
function canvasCellContentOverflows(item: RowItem, actualColIdx: number, visibleColIdx: number): boolean {
const cellWidth = renderedColumnWidths.value[visibleColIdx] ?? 0;
if (cellWidth <= 0) return false;
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 13px ${settingsStore.editorSettings.fontFamily}`);
return textWidth > Math.max(0, cellWidth - 24);
}
function canvasCellViewportRect(rowIndex: number, visibleColIdx: number) {
@ -4400,7 +4482,7 @@ const canvasEditingCell = computed(() => {
if (rowIndex < 0 || visibleColIdx < 0) return null;
const rect = canvasCellViewportRect(rowIndex, visibleColIdx);
if (!rect) return null;
return { rowIndex, visibleColIdx, actualColIdx: editing.col, rect };
return { rowId: editing.rowId, rowIndex, visibleColIdx, actualColIdx: editing.col, rect };
});
function canvasEffectiveViewportWidth(): number {
@ -4532,6 +4614,7 @@ function pauseCanvasGridWork() {
dataGridIsActive = false;
canvasResizeObserver?.disconnect();
canvasResizeObserver = null;
disconnectCellEditResizeObserver();
dataGridTopbarResizeObserver?.disconnect();
dataGridTopbarResizeObserver = null;
canvasPixelRatioMediaQueryCleanup?.();
@ -4570,6 +4653,7 @@ onUnmounted(() => {
pauseCanvasGridWork();
gridHorizontalScrollbarResizeObserver?.disconnect();
dataGridTopbarResizeObserver?.disconnect();
disconnectCellEditResizeObserver();
stopColumnHeaderDrag(false);
stopGridHorizontalScrollbarDrag();
stopGridVerticalScrollbarDrag();
@ -5879,6 +5963,130 @@ function onCellContext(rowId: number, rowIndex: number, colIdx: number, visibleC
void prefetchCopyStatements();
}
function onCellEditTextareaInput(event: Event) {
resetCellEditTextareaScrollOnResize = false;
const input = event.currentTarget as HTMLInputElement | HTMLTextAreaElement | null;
if (input instanceof HTMLTextAreaElement) {
resizeCellEditTextareaElement(input);
scheduleCellEditTextareaResize(input);
}
}
function onCellEditTextareaPaste(event: ClipboardEvent) {
const input = event.currentTarget as HTMLInputElement | HTMLTextAreaElement | null;
if (input instanceof HTMLTextAreaElement) scheduleCellEditTextareaResize(input);
}
function resizeCellEditTextareaElement(textarea: HTMLTextAreaElement | null) {
if (!textarea) return;
const textareaRect = textarea.getBoundingClientRect();
const visibleBottom = cellEditVisibleBottom(textarea);
const availableHeight = Math.max(36, Math.floor(visibleBottom - textareaRect.top - 10));
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 minHeight = Math.min(targetMinHeight, maxHeight);
textarea.style.setProperty("--cell-edit-min-height", `${minHeight}px`);
textarea.style.setProperty("--cell-edit-max-height", `${maxHeight}px`);
textarea.style.height = "auto";
textarea.style.height = `${Math.max(minHeight, Math.min(textarea.scrollHeight, maxHeight))}px`;
if (resetCellEditTextareaScrollOnResize) {
textarea.scrollTop = 0;
textarea.setSelectionRange?.(0, 0);
}
}
function cellEditTextMetrics(textarea: HTMLTextAreaElement): { lineHeight: number; verticalChrome: number } {
const computedStyle = window.getComputedStyle(textarea);
const lineHeight = Number.parseFloat(computedStyle.lineHeight) || 18;
const paddingTop = Number.parseFloat(computedStyle.paddingTop) || 0;
const paddingBottom = Number.parseFloat(computedStyle.paddingBottom) || 0;
const borderTop = Number.parseFloat(computedStyle.borderTopWidth) || 0;
const borderBottom = Number.parseFloat(computedStyle.borderBottomWidth) || 0;
return { lineHeight, verticalChrome: paddingTop + paddingBottom + borderTop + borderBottom };
}
function cellEditNaturalHeight(textarea: HTMLTextAreaElement, metrics = cellEditTextMetrics(textarea)): number {
const lines = Math.max(1, textarea.value.split(/\r\n|\r|\n/).length);
return Math.ceil(lines * metrics.lineHeight + metrics.verticalChrome);
}
function cellEditScrollerElement(textarea: HTMLTextAreaElement): HTMLElement | null {
return textarea.closest(".data-grid-scroller, .transpose-grid-scroller") as HTMLElement | null;
}
function cellEditVisibleBottom(textarea: HTMLTextAreaElement): number {
const bottoms: number[] = [];
const scroller = cellEditScrollerElement(textarea);
const root = gridRef.value;
if (scroller) bottoms.push(scroller.getBoundingClientRect().bottom);
if (root) bottoms.push(root.getBoundingClientRect().bottom);
if (typeof window !== "undefined") bottoms.push(window.innerHeight);
if (cellDetailPanelIsBottom.value && showCellDetail.value) {
const detailPanel = root?.querySelector<HTMLElement>("[data-cell-detail-panel]");
if (detailPanel) bottoms.push(detailPanel.getBoundingClientRect().top);
}
return Math.min(...bottoms.filter((bottom) => Number.isFinite(bottom)));
}
function scheduleCellEditTextareaResize(textarea: HTMLTextAreaElement | null) {
if (!textarea || typeof requestAnimationFrame !== "function") return;
requestAnimationFrame(() => {
resizeCellEditTextareaElement(textarea);
requestAnimationFrame(() => resizeCellEditTextareaElement(textarea));
});
}
function resizeActiveCellEditTextarea() {
const textarea = gridRef.value?.querySelector<HTMLTextAreaElement>(".cell-edit-input--expanded");
resizeCellEditTextareaElement(textarea ?? null);
}
function disconnectCellEditResizeObserver() {
cellEditResizeObserver?.disconnect();
cellEditResizeObserver = null;
}
function observeCellEditResizeBounds() {
disconnectCellEditResizeObserver();
if (!editingCell.value || typeof ResizeObserver === "undefined") return;
const textarea = gridRef.value?.querySelector<HTMLTextAreaElement>(".cell-edit-input--expanded");
const scroller = textarea ? cellEditScrollerElement(textarea) : null;
if (!textarea || !scroller) return;
cellEditResizeObserver = new ResizeObserver(scheduleActiveCellEditTextareaResize);
cellEditResizeObserver.observe(scroller);
cellEditResizeObserver.observe(textarea);
if (gridRef.value) cellEditResizeObserver.observe(gridRef.value);
}
function scheduleActiveCellEditTextareaResize() {
nextTick(() => {
resizeActiveCellEditTextarea();
if (typeof requestAnimationFrame === "function") {
requestAnimationFrame(() => {
resizeActiveCellEditTextarea();
requestAnimationFrame(resizeActiveCellEditTextarea);
});
}
});
}
watch(editingCell, (cell) => {
resetCellEditTextareaScrollOnResize = !!cell;
scheduleActiveCellEditTextareaResize();
if (cell) nextTick(observeCellEditResizeBounds);
else {
resetCellEditTextareaScrollOnResize = false;
expandedCellEditor.value = null;
disconnectCellEditResizeObserver();
}
});
watch(editValue, scheduleActiveCellEditTextareaResize);
function onRowContext(rowId: number, rowIndex: number) {
contextHeaderColumn.value = null;
contextHeaderColumnIndex.value = null;
@ -5969,13 +6177,17 @@ watch(
() => settingsStore.editorSettings.cellDetailDrawerWidth,
(height) => {
if (!isResizingDetail.value) detailPanelHeight.value = clampCellDetailPanelSize(height);
scheduleActiveCellEditTextareaResize();
},
);
watch(cellDetailPanelLayout, (layout) => {
if (!isResizingDetail.value) detailPanelHeight.value = clampCellDetailPanelSize(detailPanelHeight.value, layout);
scheduleActiveCellEditTextareaResize();
});
watch([showCellDetail, activeCellDetail, cellDetailPanelIsBottom, detailPanelHeight], scheduleActiveCellEditTextareaResize);
const ddlDrawerStyle = computed(() => ({
width: `${ddlWidth.value}px`,
}));
@ -6608,7 +6820,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
</script>
<template>
<div ref="gridRef" data-grid-root class="h-full flex flex-col overflow-hidden outline-none" :style="gridStyle" tabindex="0" @keydown="onGridKeydown" @paste="onGridPaste">
<div ref="gridRef" data-grid-root class="h-full flex flex-col overflow-hidden outline-none" :class="{ 'data-grid--editing-cell': !!editingCell }" :style="gridStyle" tabindex="0" @keydown="onGridKeydown" @paste="onGridPaste">
<CustomContextMenu :items="gridContextMenuItems" v-slot="{ onContextMenu }">
<div v-if="hasData || canShowWhereSearch" class="flex-1 flex flex-col overflow-hidden" @contextmenu="onContextMenu">
<!-- Search bar -->
@ -7161,22 +7373,41 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@mouseenter="onTransposeCellMouseenter(cell.recordIndex, cell.valueIndex)"
@mouseleave="onCellMouseleave(cell.recordIndex, cell.valueIndex)"
@contextmenu="onTransposeCellContext(cell.recordIndex, cell.valueIndex, $event)"
@dblclick.stop="canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex) && startEdit(displayItems[cell.recordIndex].id, cell.valueIndex)"
@dblclick.stop="canEditCellItem(displayItems[cell.recordIndex], cell.valueIndex) && startDomCellEdit(displayItems[cell.recordIndex].id, cell.valueIndex, cell.display, $event)"
>
<template v-if="editingCell?.rowId === displayItems[cell.recordIndex]?.id && editingCell?.col === cell.valueIndex">
<TemporalCellEditor v-if="temporalEditorKindForColumn(cell.valueIndex)" v-model="editValue" :kind="temporalEditorKindForColumn(cell.valueIndex)!" cell-layout="transpose" @cancel="cancelEdit" @commit="commitGridEdit" />
<EnumCellEditor v-else-if="isEnumGridColumn(cell.valueIndex)" v-model="editValue" :values="enumValuesForGridColumn(cell.valueIndex)" :nullable="isEnumGridColumnNullable(cell.valueIndex)" cell-layout="transpose" @cancel="cancelEdit" @commit="commitGridEdit" />
<textarea
v-else-if="cellUsesExpandedEditor(displayItems[cell.recordIndex]?.id, cell.valueIndex)"
v-model="editValue"
data-expanded-cell-editor="true"
rows="1"
:inputmode="cellEditInputModeForColumn(cell.valueIndex)"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="cell-edit-input cell-edit-input--expanded absolute left-0 top-0 min-h-full bg-background px-1.5 py-1 text-[13px] leading-[18px] outline-none z-10"
@blur="commitEditFromBlur"
@click.stop
@focus="onCellEditTextareaInput"
@input="onCellEditTextareaInput"
@keydown.stop="onEditKeydown"
@paste.stop="onCellEditTextareaPaste"
/>
<input
v-else
v-model="editValue"
:inputmode="cellEditInputModeForColumn(cell.valueIndex)"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="cell-edit-input absolute inset-0 bg-background border-2 border-primary px-1.5 py-0 text-[13px] leading-[26px] outline-none z-10"
@blur="commitEditFromBlur"
@click.stop
@input="onCellEditTextareaInput"
@keydown.stop="onEditKeydown"
@paste.stop
@paste.stop="onCellEditTextareaPaste"
/>
</template>
<template v-else>
@ -7576,7 +7807,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@contextmenu="onCanvasContext"
@dblclick="onCanvasDblClick"
/>
<div ref="canvasOverlayRef" class="canvas-grid-overlay dbx-data-grid-font-family sticky left-0 top-0 z-10 overflow-hidden" :style="canvasOverlayStyle">
<div ref="canvasOverlayRef" class="canvas-grid-overlay dbx-data-grid-font-family sticky left-0 top-0 z-10 overflow-visible" :style="canvasOverlayStyle">
<div v-if="canvasEditingCell" class="absolute pointer-events-auto z-20 tabular-nums" :style="canvasEditingCellStyle" @mousedown.stop @click.stop>
<TemporalCellEditor v-if="temporalEditorKindForColumn(canvasEditingCell.actualColIdx)" v-model="editValue" :kind="temporalEditorKindForColumn(canvasEditingCell.actualColIdx)!" @cancel="cancelEdit" @commit="commitGridEdit" />
<EnumCellEditor
@ -7587,17 +7818,36 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
@cancel="cancelEdit"
@commit="commitGridEdit"
/>
<textarea
v-else-if="cellUsesExpandedEditor(canvasEditingCell.rowId, canvasEditingCell.actualColIdx)"
v-model="editValue"
data-expanded-cell-editor="true"
rows="1"
:inputmode="cellEditInputModeForColumn(canvasEditingCell.actualColIdx)"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="cell-edit-input cell-edit-input--expanded absolute left-0 top-0 min-h-full bg-background px-2.5 py-1 text-[13px] leading-[18px] outline-none z-10"
@blur="commitEditFromBlur"
@click.stop
@focus="onCellEditTextareaInput"
@input="onCellEditTextareaInput"
@keydown.stop="onEditKeydown"
@paste.stop="onCellEditTextareaPaste"
/>
<input
v-else
v-model="editValue"
:inputmode="cellEditInputModeForColumn(canvasEditingCell.actualColIdx)"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="cell-edit-input absolute inset-0 bg-background border-2 border-primary px-2.5 py-0 text-[13px] leading-[22px] outline-none z-10"
@blur="commitEditFromBlur"
@click.stop
@input="onCellEditTextareaInput"
@keydown.stop="onEditKeydown"
@paste.stop
@paste.stop="onCellEditTextareaPaste"
/>
</div>
<div v-if="canvasDetailButtonCell" class="absolute pointer-events-auto z-20 flex items-center gap-1" :style="canvasDetailButtonStyle" @mouseenter="keepCanvasDetailHover" @mouseleave="clearCanvasDetailHover">
@ -7656,6 +7906,7 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
'bg-primary/5': item.isNew && !isRowActive(item.displayIndex),
'bg-muted/30': !item.isNew && !item.isDeleted && !isRowActive(item.displayIndex) && item.displayIndex % 2 === 1,
'active-row': isRowActive(item.displayIndex) && !item.isDeleted,
'relative z-20 overflow-visible': editingCell?.rowId === item.id,
}"
:data-row-index="item.displayIndex"
>
@ -7686,28 +7937,49 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
'tabular-nums': typeof item.data[col.actualColIdx] === 'number',
'cursor-text hover:bg-gray-200 dark:hover:bg-gray-800': !isScrolling && canEditCellItem(item, col.actualColIdx),
'line-through': item.isDeleted,
'overflow-visible z-20 border-r-transparent': editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx,
'overflow-hidden': !(editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx),
}"
@mousedown="handleDataCellMousedown(item.displayIndex, col.visibleColIdx, item.id, $event)"
@mouseenter="onCellMouseenter(item.displayIndex, col.visibleColIdx, col.actualColIdx)"
@mouseleave="onCellMouseleave(item.displayIndex, col.actualColIdx)"
@dblclick="canEditCellItem(item, col.actualColIdx) && startEdit(item.id, col.actualColIdx)"
@dblclick="canEditCellItem(item, col.actualColIdx) && startDomCellEdit(item.id, col.actualColIdx, formatCellCached(item.data[col.actualColIdx], col.actualColIdx), $event)"
:data-visible-col-index="col.visibleColIdx"
@contextmenu="onCellContext(item.id, item.displayIndex, col.actualColIdx, col.visibleColIdx)"
>
<template v-if="editingCell?.rowId === item.id && editingCell?.col === col.actualColIdx">
<TemporalCellEditor v-if="temporalEditorKindForColumn(col.actualColIdx)" v-model="editValue" :kind="temporalEditorKindForColumn(col.actualColIdx)!" @cancel="cancelEdit" @commit="commitGridEdit" />
<EnumCellEditor v-else-if="isEnumGridColumn(col.actualColIdx)" v-model="editValue" :values="enumValuesForGridColumn(col.actualColIdx)" :nullable="isEnumGridColumnNullable(col.actualColIdx)" @cancel="cancelEdit" @commit="commitGridEdit" />
<textarea
v-else-if="cellUsesExpandedEditor(item.id, col.actualColIdx)"
v-model="editValue"
data-expanded-cell-editor="true"
rows="1"
:inputmode="cellEditInputModeForColumn(col.actualColIdx)"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="cell-edit-input cell-edit-input--expanded absolute left-0 top-0 min-h-full bg-background px-2.5 py-1 text-[13px] leading-[18px] outline-none z-10"
@blur="commitEditFromBlur"
@click.stop
@focus="onCellEditTextareaInput"
@input="onCellEditTextareaInput"
@keydown.stop="onEditKeydown"
@paste.stop="onCellEditTextareaPaste"
/>
<input
v-else
v-model="editValue"
:inputmode="cellEditInputModeForColumn(col.actualColIdx)"
autocapitalize="off"
autocorrect="off"
spellcheck="false"
class="cell-edit-input absolute inset-0 bg-background border-2 border-primary px-2.5 py-0 text-[13px] leading-[22px] outline-none z-10"
@blur="commitEditFromBlur"
@click.stop
@input="onCellEditTextareaInput"
@keydown.stop="onEditKeydown"
@paste.stop
@paste.stop="onCellEditTextareaPaste"
/>
</template>
<template v-else>
@ -8812,6 +9084,18 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
contain: layout style paint;
}
[data-grid-root].data-grid--editing-cell .data-grid-scroller :deep(.vue-recycle-scroller__item-view),
[data-grid-root].data-grid--editing-cell .transpose-grid-scroller :deep(.vue-recycle-scroller__item-view) {
contain: layout style;
overflow: visible;
}
[data-grid-root] .data-grid-scroller :deep(.vue-recycle-scroller__item-view:has(.cell-edit-input--expanded)),
[data-grid-root] .transpose-grid-scroller :deep(.vue-recycle-scroller__item-view:has(.cell-edit-input--expanded)) {
z-index: 80 !important;
overflow: visible;
}
.data-grid-scroller.is-scrolling :deep(.vue-recycle-scroller__item-view) {
pointer-events: none;
}
@ -8900,6 +9184,58 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
font-family: inherit;
}
.cell-edit-input--expanded {
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;
scrollbar-width: thin;
scrollbar-color: color-mix(in oklab, var(--foreground) 24%, transparent) transparent;
resize: none;
white-space: pre-wrap;
overflow-wrap: anywhere;
background-color: var(--background);
background-color: color-mix(in oklab, var(--background) 96%, var(--primary) 4%);
border: 1px solid color-mix(in oklab, var(--primary) 62%, var(--border));
border-radius: 6px;
z-index: 90;
box-shadow:
0 28px 72px rgb(0 0 0 / 34%),
0 12px 30px rgb(0 0 0 / 24%),
0 3px 10px rgb(0 0 0 / 18%),
0 0 0 1px var(--background),
inset 0 0 0 1px color-mix(in oklab, var(--background) 70%, transparent);
}
:global(.dark) .cell-edit-input--expanded {
box-shadow:
0 0 0 1px color-mix(in oklab, var(--foreground) 26%, transparent),
0 0 34px color-mix(in oklab, var(--foreground) 24%, transparent),
0 0 70px color-mix(in oklab, var(--foreground) 14%, transparent),
0 24px 64px rgb(0 0 0 / 42%),
inset 0 0 0 1px color-mix(in oklab, var(--background) 58%, transparent);
}
.cell-edit-input--expanded::-webkit-scrollbar {
width: 4px;
}
.cell-edit-input--expanded::-webkit-scrollbar-thumb {
border-radius: 999px;
background: color-mix(in oklab, var(--foreground) 24%, transparent);
}
.cell-edit-input--expanded:hover::-webkit-scrollbar-thumb,
.cell-edit-input--expanded:focus::-webkit-scrollbar-thumb {
background: color-mix(in oklab, var(--foreground) 42%, transparent);
}
.cell-edit-input--expanded:hover::-webkit-scrollbar,
.cell-edit-input--expanded:focus::-webkit-scrollbar {
width: 6px;
}
.canvas-grid-overlay {
pointer-events: none;
}

View File

@ -204,11 +204,16 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
const focusInput = () => {
if (typeof document === "undefined") return;
const root = getScrollerElement()?.closest("[data-grid-root]");
const input = (root ?? document).querySelector(".cell-edit-input") as HTMLInputElement | null;
const input = (root ?? document).querySelector(".cell-edit-input") as HTMLInputElement | HTMLTextAreaElement | null;
input?.focus();
if (select && input) {
input.select();
input.setSelectionRange?.(0, input.value.length);
if (input instanceof HTMLTextAreaElement && input.dataset.expandedCellEditor === "true") {
input.setSelectionRange?.(0, 0);
input.scrollTop = 0;
} else {
input.select();
input.setSelectionRange?.(0, input.value.length);
}
}
};
nextTick(() => {
@ -599,7 +604,8 @@ export function useDataGridEditor(options: UseDataGridEditorOptions) {
}
function onEditKeydown(e: KeyboardEvent) {
if (e.key === "Enter") {
const isExpandedTextarea = e.target instanceof HTMLTextAreaElement && e.target.dataset.expandedCellEditor === "true";
if (e.key === "Enter" && (!isExpandedTextarea || e.metaKey || e.ctrlKey)) {
e.preventDefault();
commitEdit();
nextTick(focusScrollerWithoutScrolling);