diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index f353a425fb..2682a1ae76 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -132,6 +132,7 @@ import { allNullColumnIndexes, filterColumnVisibilityOptions, hiddenColumnIndexe import { columnOrderKeysForIndexes, isDefaultColumnOrder, moveVisibleColumnIndex, orderedColumnIndexes, uniqueDataGridColumnOrderKeys } from "@/lib/dataGrid/dataGridColumnOrder"; import { dataGridColumnLayoutScopeKey, loadDataGridColumnOrder, removeDataGridColumnOrder, saveDataGridColumnOrder } from "@/lib/dataGrid/dataGridColumnLayoutStorage"; import { parseClipboardTable, summarizeSelection } from "@/lib/dataGrid/gridSelection"; +import { columnHeaderCanvasPointerDisabled, columnHeaderClickShouldBeSuppressed, columnHeaderPreviewOffsetForColumn, columnHeaderTooltipDisabled } from "@/lib/dataGrid/dataGridColumnHeaderInteraction"; import { useToast } from "@/composables/useToast"; import { useDataGridExport } from "@/composables/useDataGridExport"; @@ -3019,7 +3020,78 @@ type ColumnHeaderDragState = { dragging: boolean; }; const columnHeaderDragState = ref(null); +const columnHeaderResizeActive = ref(false); let columnHeaderDragClickGuardUntil = 0; +let columnHeaderSuppressNextClick = false; +let columnHeaderSuppressClickTimer = 0; +let columnHeaderDragFrame = 0; +let columnHeaderPendingClientX = 0; +let columnHeaderResizeListenersCleanup: (() => void) | null = null; + +const columnHeaderTooltipsDisabled = computed(() => + columnHeaderTooltipDisabled({ + columnDragActive: columnHeaderDragState.value !== null, + columnResizeActive: columnHeaderResizeActive.value, + }), +); + +function columnHeaderPointerInteractionActive(): boolean { + return columnHeaderCanvasPointerDisabled({ + columnDragActive: columnHeaderDragState.value !== null, + columnResizeActive: columnHeaderResizeActive.value, + }); +} + +function finishColumnHeaderResizeInteraction() { + clearColumnHeaderResizeListeners(); + requestAnimationFrame(() => { + columnHeaderResizeActive.value = false; + }); +} + +function clearColumnHeaderResizeListeners() { + if (!columnHeaderResizeListenersCleanup) return; + columnHeaderResizeListenersCleanup(); + columnHeaderResizeListenersCleanup = null; +} + +function clearColumnHeaderClickGuard() { + columnHeaderSuppressNextClick = false; + columnHeaderDragClickGuardUntil = 0; + if (columnHeaderSuppressClickTimer) { + window.clearTimeout(columnHeaderSuppressClickTimer); + columnHeaderSuppressClickTimer = 0; + } +} + +function armColumnHeaderClickGuard() { + clearColumnHeaderClickGuard(); + columnHeaderSuppressNextClick = true; + columnHeaderDragClickGuardUntil = Date.now() + 800; + columnHeaderSuppressClickTimer = window.setTimeout(() => { + clearColumnHeaderClickGuard(); + }, 800); +} + +function startColumnHeaderResize(visibleColIdx: number, event: MouseEvent) { + // Browser hover state can stick to multiple header cells during pointer drags, + // so keep header tooltips closed until the resize gesture has fully settled. + clearColumnHeaderResizeListeners(); + columnHeaderResizeActive.value = true; + armColumnHeaderClickGuard(); + onCanvasMouseLeave(); + const finishResize = () => { + armColumnHeaderClickGuard(); + finishColumnHeaderResizeInteraction(); + }; + columnHeaderResizeListenersCleanup = () => { + window.removeEventListener("mouseup", finishResize, true); + window.removeEventListener("blur", finishResize, true); + }; + window.addEventListener("mouseup", finishResize, true); + window.addEventListener("blur", finishResize, true); + onResizeStart(visibleColIdx, event); +} function columnHeaderInteractiveTarget(target: EventTarget | null): boolean { return target instanceof HTMLElement && !!target.closest("button, input, textarea, select, [contenteditable='true'], [role='button'], [data-column-resize-handle]"); @@ -3036,6 +3108,32 @@ function columnHeaderDropTargetVisibleIndex(clientX: number): number { return visibleColumnIndexes.value.length; } +function applyColumnHeaderDragPreview() { + columnHeaderDragFrame = 0; + const state = columnHeaderDragState.value; + if (!state?.dragging) return; + state.currentX = columnHeaderPendingClientX; + state.targetVisibleIndex = columnHeaderDropTargetVisibleIndex(columnHeaderPendingClientX); + if (useCanvasGridRows.value) scheduleCanvasDraw(); +} + +function scheduleColumnHeaderDragPreview(clientX: number) { + columnHeaderPendingClientX = clientX; + if (columnHeaderDragFrame) return; + columnHeaderDragFrame = requestAnimationFrame(applyColumnHeaderDragPreview); +} + +function flushColumnHeaderDragPreview() { + if (columnHeaderDragFrame) cancelAnimationFrame(columnHeaderDragFrame); + applyColumnHeaderDragPreview(); +} + +function cancelColumnHeaderDragPreview() { + if (!columnHeaderDragFrame) return; + cancelAnimationFrame(columnHeaderDragFrame); + columnHeaderDragFrame = 0; +} + function columnHeaderLayoutRects(): { visibleIndex: number; left: number; width: number }[] { const header = headerRef.value; return Array.from(header?.querySelectorAll("[data-visible-col-index]") ?? []) @@ -3053,12 +3151,15 @@ function columnHeaderLayoutRects(): { visibleIndex: number; left: number; width: function stopColumnHeaderDrag(commit: boolean) { const state = columnHeaderDragState.value; if (!state) return; + const hadCanvasPreview = useCanvasGridRows.value && state.dragging; window.removeEventListener("pointermove", onColumnHeaderPointerMove, true); window.removeEventListener("pointerup", onColumnHeaderPointerUp, true); window.removeEventListener("pointercancel", onColumnHeaderPointerCancel, true); + cancelColumnHeaderDragPreview(); document.body.style.userSelect = ""; columnHeaderDragState.value = null; - if (state.dragging) columnHeaderDragClickGuardUntil = Date.now() + 250; + if (hadCanvasPreview) scheduleCanvasDraw(); + if (state.dragging) armColumnHeaderClickGuard(); if (!commit || !state.dragging || state.sourceVisibleIndex === state.targetVisibleIndex) return; const next = moveVisibleColumnIndex({ orderedIndexes: orderedDisplayableColumnIndexes.value, @@ -3077,14 +3178,16 @@ function onColumnHeaderPointerMove(event: PointerEvent) { if (!state.dragging && moved) { state.dragging = true; document.body.style.userSelect = "none"; + onCanvasMouseLeave(); } if (!state.dragging) return; event.preventDefault(); - state.currentX = event.clientX; - state.targetVisibleIndex = columnHeaderDropTargetVisibleIndex(event.clientX); + scheduleColumnHeaderDragPreview(event.clientX); } -function onColumnHeaderPointerUp() { +function onColumnHeaderPointerUp(event: PointerEvent) { + columnHeaderPendingClientX = event.clientX; + flushColumnHeaderDragPreview(); stopColumnHeaderDrag(true); } @@ -3103,17 +3206,35 @@ function startColumnHeaderDrag(visibleColIdx: number, event: PointerEvent) { columnRects: columnHeaderLayoutRects(), dragging: false, }; + columnHeaderPendingClientX = event.clientX; window.addEventListener("pointermove", onColumnHeaderPointerMove, true); window.addEventListener("pointerup", onColumnHeaderPointerUp, true); window.addEventListener("pointercancel", onColumnHeaderPointerCancel, true); } -function onHeaderClick(visibleColIdx: number, event: MouseEvent) { - if (Date.now() < columnHeaderDragClickGuardUntil) { - event.preventDefault(); - event.stopPropagation(); - return; +function suppressHeaderClickIfNeeded(event: MouseEvent): boolean { + if ( + !columnHeaderClickShouldBeSuppressed({ + now: Date.now(), + guardUntil: columnHeaderDragClickGuardUntil, + suppressNextClick: columnHeaderSuppressNextClick, + }) + ) { + return false; } + clearColumnHeaderClickGuard(); + event.preventDefault(); + event.stopPropagation(); + event.stopImmediatePropagation(); + return true; +} + +function onHeaderClickCapture(event: MouseEvent) { + suppressHeaderClickIfNeeded(event); +} + +function onHeaderClick(visibleColIdx: number, event: MouseEvent) { + if (suppressHeaderClickIfNeeded(event)) return; selectColumn(visibleColIdx, event); } @@ -3126,14 +3247,16 @@ function columnHeaderDragClass(visibleColIdx: number) { function columnHeaderPreviewOffset(visibleColIdx: number): number { const state = columnHeaderDragState.value; - if (!state?.dragging) return 0; - const sourceIndex = state.sourceVisibleIndex; - if (visibleColIdx === sourceIndex) return state.currentX - state.startX; - const targetIndex = state.targetVisibleIndex; - const sourceWidth = renderedColumnWidths.value[sourceIndex] ?? 0; - if (targetIndex < sourceIndex && visibleColIdx >= targetIndex && visibleColIdx < sourceIndex) return sourceWidth; - if (targetIndex > sourceIndex && visibleColIdx > sourceIndex && visibleColIdx <= targetIndex) return -sourceWidth; - return 0; + if (!state) return 0; + return columnHeaderPreviewOffsetForColumn({ + columnDragActive: state.dragging, + visibleColIdx, + sourceVisibleIndex: state.sourceVisibleIndex, + targetVisibleIndex: state.targetVisibleIndex, + startX: state.startX, + currentX: state.currentX, + sourceWidth: renderedColumnWidths.value[state.sourceVisibleIndex] ?? 0, + }); } function columnHeaderStyle(visibleColIdx: number) { @@ -3147,6 +3270,12 @@ function columnHeaderStyle(visibleColIdx: number) { }; } +const columnHeaderPreviewOffsets = computed(() => renderedColumnWidths.value.map((_, visibleColIdx) => columnHeaderPreviewOffset(visibleColIdx))); +const columnHeaderPreviewSourceVisibleIndex = computed(() => { + const state = columnHeaderDragState.value; + return state?.dragging ? state.sourceVisibleIndex : null; +}); + function columnContentOffsetLeft(visibleColIdx: number): number { return DATA_GRID_ROW_NUM_WIDTH + (renderedColumnOffsets.value[visibleColIdx] ?? 0); } @@ -5462,6 +5591,11 @@ function onCanvasWheel(event: WheelEvent) { } function onCanvasMouseMove(event: MouseEvent) { + if (columnHeaderPointerInteractionActive()) { + if (canvasRef.value) canvasRef.value.style.cursor = "default"; + onCanvasMouseLeave(); + return; + } const hit = canvasHitTest(event); const hitItem = hit ? displayItemAt(hit.rowIndex) : undefined; const next = hit && hitItem ? { rowIndex: hitItem.displayIndex, visibleColIdx: hit.rowNumber ? -1 : hit.visibleColIdx } : null; @@ -5698,6 +5832,8 @@ function drawCanvasGrid() { rowAt: displayItemAt, renderedColumnWidths: renderedColumnWidths.value, renderedColumnOffsets: renderedColumnOffsets.value, + columnPreviewOffsets: columnHeaderPreviewOffsets.value, + columnPreviewSourceVisibleIndex: columnHeaderPreviewSourceVisibleIndex.value, visibleColumnIndexes: visibleColumnIndexes.value, rowNumberWidth: DATA_GRID_ROW_NUM_WIDTH, hoverCell: canvasHoverCell.value, @@ -7795,6 +7931,10 @@ onDeactivated(() => { onUnmounted(() => { cleanupFrames(); stopAutoRefreshTimer(); + cancelColumnHeaderDragPreview(); + clearColumnHeaderResizeListeners(); + clearColumnHeaderClickGuard(); + columnHeaderResizeActive.value = false; onSearchSplitResizeEnd(); onDdlResizeEnd(); onDetailResizeEnd(); @@ -9016,7 +9156,7 @@ const gridContextMenuItems = computed(() => { #
- +
@@ -9352,7 +9493,7 @@ const gridContextMenuItems = computed(() => { -
+