diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index 21c9d22b9..e5d6649c6 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -2458,13 +2458,17 @@ const gridVerticalScrollbarThumbHeightPercent = ref(100); const gridHorizontalScrollbarDragging = ref(false); const gridVerticalScrollbarDragging = ref(false); let gridHorizontalScrollbarFrame = 0; +let gridHorizontalScrollbarDragFrame = 0; +let gridHorizontalScrollbarPendingClientX = 0; let gridHorizontalScrollbarResizeObserver: ResizeObserver | null = null; let dataGridTopbarResizeObserver: ResizeObserver | null = null; let cellEditResizeObserver: ResizeObserver | null = null; let resetCellEditTextareaScrollOnResize = false; let gridHorizontalScrollbarDragState: { + scroller: HTMLElement; trackRect: DOMRect; thumbOffsetPx: number; + maxScrollLeft: number; } | null = null; let gridVerticalScrollbarDragState: { trackRect: DOMRect; @@ -2790,33 +2794,47 @@ function observeDataGridTopbarWidth() { } } -function applyGridHorizontalScrollbarDrag(clientX: number) { - const scroller = gridScrollerElement(); +function applyPendingGridHorizontalScrollbarDrag() { + gridHorizontalScrollbarDragFrame = 0; const dragState = gridHorizontalScrollbarDragState; - if (!scroller || !dragState) return; - - const maxScrollLeft = Math.max(0, scroller.scrollWidth - scroller.clientWidth); - if (maxScrollLeft <= 1) return; + if (!dragState) return; const thumbWidthPx = dragState.trackRect.width * (gridHorizontalScrollbarThumbWidthPercent.value / 100); const maxThumbLeftPx = Math.max(1, dragState.trackRect.width - thumbWidthPx); - const thumbLeftPx = Math.min(maxThumbLeftPx, Math.max(0, clientX - dragState.trackRect.left - dragState.thumbOffsetPx)); - scroller.scrollLeft = (thumbLeftPx / maxThumbLeftPx) * maxScrollLeft; + const thumbLeftPx = Math.min(maxThumbLeftPx, Math.max(0, gridHorizontalScrollbarPendingClientX - dragState.trackRect.left - dragState.thumbOffsetPx)); + const scroller = dragState.scroller; + const nextScrollLeft = (thumbLeftPx / maxThumbLeftPx) * dragState.maxScrollLeft; + if (Math.abs(scroller.scrollLeft - nextScrollLeft) < 0.5) return; + scroller.scrollLeft = nextScrollLeft; updateGridHorizontalViewport(scroller); if (headerRef.value) headerRef.value.scrollLeft = scroller.scrollLeft; if (useCanvasGridRows.value) { - syncCanvasViewport(); + drawCanvasGridNow(); } } +function scheduleGridHorizontalScrollbarDrag(clientX: number) { + gridHorizontalScrollbarPendingClientX = clientX; + if (gridHorizontalScrollbarDragFrame) return; + // Pointermove can fire faster than paint; keep expensive canvas/layout work to one frame. + gridHorizontalScrollbarDragFrame = requestAnimationFrame(applyPendingGridHorizontalScrollbarDrag); +} + +function flushGridHorizontalScrollbarDrag() { + if (!gridHorizontalScrollbarDragFrame) return; + cancelAnimationFrame(gridHorizontalScrollbarDragFrame); + applyPendingGridHorizontalScrollbarDrag(); +} + function onGridHorizontalScrollbarPointerMove(event: PointerEvent) { if (!gridHorizontalScrollbarDragState) return; event.preventDefault(); - applyGridHorizontalScrollbarDrag(event.clientX); + scheduleGridHorizontalScrollbarDrag(event.clientX); } function stopGridHorizontalScrollbarDrag() { if (!gridHorizontalScrollbarDragState) return; + flushGridHorizontalScrollbarDrag(); gridHorizontalScrollbarDragState = null; gridHorizontalScrollbarDragging.value = false; window.removeEventListener("pointermove", onGridHorizontalScrollbarPointerMove, true); @@ -2840,6 +2858,8 @@ function startGridHorizontalScrollbarDrag(event: PointerEvent) { const track = gridHorizontalScrollbarTrackRef.value; if (!scroller || !track || !hasGridHorizontalOverflow.value) return; + const maxScrollLeft = Math.max(0, scroller.scrollWidth - scroller.clientWidth); + if (maxScrollLeft <= 1) return; const trackRect = track.getBoundingClientRect(); const thumbLeftPx = trackRect.width * (gridHorizontalScrollbarThumbLeftPercent.value / 100); const thumbWidthPx = trackRect.width * (gridHorizontalScrollbarThumbWidthPercent.value / 100); @@ -2847,8 +2867,10 @@ function startGridHorizontalScrollbarDrag(event: PointerEvent) { const pointerInsideThumb = pointerX >= thumbLeftPx && pointerX <= thumbLeftPx + thumbWidthPx; gridHorizontalScrollbarDragState = { + scroller, trackRect, thumbOffsetPx: pointerInsideThumb ? pointerX - thumbLeftPx : thumbWidthPx / 2, + maxScrollLeft, }; gridHorizontalScrollbarDragging.value = true; document.body.style.userSelect = "none"; @@ -2856,7 +2878,7 @@ function startGridHorizontalScrollbarDrag(event: PointerEvent) { window.addEventListener("pointerup", stopGridHorizontalScrollbarDrag, true); window.addEventListener("pointercancel", stopGridHorizontalScrollbarDrag, true); event.preventDefault(); - applyGridHorizontalScrollbarDrag(event.clientX); + scheduleGridHorizontalScrollbarDrag(event.clientX); } function applyGridVerticalScrollbarDrag(clientY: number) { diff --git a/apps/desktop/src/components/sidebar/TreeItem.vue b/apps/desktop/src/components/sidebar/TreeItem.vue index 49b80c7b0..49285c8aa 100644 --- a/apps/desktop/src/components/sidebar/TreeItem.vue +++ b/apps/desktop/src/components/sidebar/TreeItem.vue @@ -773,6 +773,11 @@ function selectedTreeNodesInVisibleOrder(): TreeNode[] { } function selectSingleTreeNode(node: TreeNode) { + // Re-clicking the selected row should not replace the selection array and + // force visible tree rows to recompute. + if (!connectionStore.connectionMultiSelectActive && connectionStore.selectedTreeNodeId === node.id && connectionStore.treeSelectionAnchorId === node.id && connectionStore.selectedTreeNodeIds.length === 1 && connectionStore.selectedTreeNodeIds[0] === node.id) { + return; + } connectionStore.connectionMultiSelectActive = false; connectionStore.selectedTreeNodeId = node.id; connectionStore.selectedTreeNodeIds = [node.id]; @@ -5134,7 +5139,7 @@ function treeItemMenuItems(): ContextMenuItem[] { - +
ContextMenuItem[]); + const props = defineProps<{ - items: ContextMenuItem[]; + items: ContextMenuItemsSource; }>(); defineEmits<{ @@ -51,6 +53,7 @@ const show = ref(false); const x = ref(0); const y = ref(0); const menuRef = ref(); +const activeItems = ref([]); // Submenu state const activeSubIndex = ref(null); @@ -63,6 +66,7 @@ let subAnchorRect: { left: number; right: number; top: number; bottom: number } function close() { activeSubIndex.value = null; subAnchorRect = null; + activeItems.value = []; show.value = false; } @@ -122,7 +126,10 @@ function handleSubItemClick(item: ContextMenuItem) { } function onContextMenu(event: MouseEvent) { - if (props.items.length === 0) return; + // Some callers build large context menus; resolve them only for actual opens. + const items = typeof props.items === "function" ? props.items() : props.items; + if (items.length === 0) return; + activeItems.value = items; event.preventDefault(); event.stopPropagation(); x.value = event.clientX; @@ -143,7 +150,7 @@ function onContextMenu(event: MouseEvent) { function onItemMouseEnter(index: number, event: MouseEvent) { lastMouseX = event.clientX; lastMouseY = event.clientY; - const item = props.items[index]; + const item = activeItems.value[index]; if (!item?.children?.length || item.disabled) { // Moving to an item without children — close submenu immediately, no delay needed activeSubIndex.value = null; @@ -259,7 +266,7 @@ onBeforeUnmount(() => {
-