feat(grid): add expanded overflow cell editor
This commit is contained in:
parent
28e7cdfacc
commit
a25bd5441f
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in New Issue