diff --git a/apps/desktop/src/components/grid/DataGrid.vue b/apps/desktop/src/components/grid/DataGrid.vue index f6a0ec7eb..020fe5432 100644 --- a/apps/desktop/src/components/grid/DataGrid.vue +++ b/apps/desktop/src/components/grid/DataGrid.vue @@ -2467,15 +2467,15 @@ const dataGridTopbarRef = ref(); const headerRef = ref(); const gridScrollbarGutter = ref(0); const gridHorizontalScrollbarTrackRef = ref(); +const gridHorizontalScrollbarThumbRef = ref(); const gridVerticalScrollbarTrackRef = ref(); +const gridVerticalScrollbarThumbRef = ref(); const hasGridHorizontalOverflow = ref(false); const hasGridVerticalOverflow = ref(false); -const gridHorizontalScrollbarThumbLeftPercent = ref(0); -const gridHorizontalScrollbarThumbWidthPercent = ref(100); -const gridVerticalScrollbarThumbTopPercent = ref(0); -const gridVerticalScrollbarThumbHeightPercent = ref(100); -const gridHorizontalScrollbarDragging = ref(false); -const gridVerticalScrollbarDragging = ref(false); +let gridHorizontalScrollbarThumbLeftPercent = 0; +let gridHorizontalScrollbarThumbWidthPercent = 100; +let gridVerticalScrollbarThumbTopPercent = 0; +let gridVerticalScrollbarThumbHeightPercent = 100; let gridHorizontalScrollbarFrame = 0; let gridHorizontalScrollbarDragFrame = 0; let gridHorizontalScrollbarPendingClientX = 0; @@ -2497,6 +2497,8 @@ let gridVerticalScrollbarDragState: { thumbOffsetPx: number; maxScrollTop: number; } | null = null; +const GRID_HORIZONTAL_SCROLLBAR_DRAGGING_CLASS = "data-grid-horizontal-scrollbar--dragging"; +const GRID_VERTICAL_SCROLLBAR_DRAGGING_CLASS = "data-grid-vertical-scrollbar--dragging"; const hiddenColumnIndexes = ref>(new Set()); const nullColumnsHidden = ref(false); const autoHiddenNullColumnIndexes = ref>(new Set()); @@ -2728,9 +2730,12 @@ const renderedColumnOffsets = computed(() => { }); function updateGridHorizontalViewport(element: HTMLElement) { - gridHorizontalScrollLeft.value = element.scrollLeft; - gridViewportWidth.value = element.clientWidth; - updateGridHorizontalScrollbar(element); + const nextScrollLeft = element.scrollLeft; + const nextViewportWidth = element.clientWidth; + const horizontalChanged = gridHorizontalScrollLeft.value !== nextScrollLeft || gridViewportWidth.value !== nextViewportWidth; + if (gridHorizontalScrollLeft.value !== nextScrollLeft) gridHorizontalScrollLeft.value = nextScrollLeft; + if (gridViewportWidth.value !== nextViewportWidth) gridViewportWidth.value = nextViewportWidth; + if (horizontalChanged) updateGridHorizontalScrollbar(element); updateGridVerticalScrollbar(element); } @@ -2740,38 +2745,84 @@ function gridScrollerElement(): HTMLElement | null { function updateGridHorizontalScrollbar(element: HTMLElement | null = gridScrollerElement()) { if (!element) { - hasGridHorizontalOverflow.value = false; - gridHorizontalScrollbarThumbLeftPercent.value = 0; - gridHorizontalScrollbarThumbWidthPercent.value = 100; + setGridHorizontalOverflow(false); + gridHorizontalScrollbarThumbLeftPercent = 0; + gridHorizontalScrollbarThumbWidthPercent = 100; + applyGridHorizontalScrollbarThumbStyle(); return; } const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth); - hasGridHorizontalOverflow.value = maxScrollLeft > 1; + setGridHorizontalOverflow(maxScrollLeft > 1); const rawThumbWidth = element.scrollWidth > 0 ? (element.clientWidth / element.scrollWidth) * 100 : 100; const thumbWidth = Math.min(100, Math.max(6, rawThumbWidth)); const thumbTravel = Math.max(0, 100 - thumbWidth); - gridHorizontalScrollbarThumbWidthPercent.value = thumbWidth; - gridHorizontalScrollbarThumbLeftPercent.value = maxScrollLeft > 0 ? (element.scrollLeft / maxScrollLeft) * thumbTravel : 0; + gridHorizontalScrollbarThumbWidthPercent = thumbWidth; + gridHorizontalScrollbarThumbLeftPercent = maxScrollLeft > 0 ? (element.scrollLeft / maxScrollLeft) * thumbTravel : 0; + if (!applyGridHorizontalScrollbarThumbStyle() && hasGridHorizontalOverflow.value) { + nextTick(applyGridHorizontalScrollbarThumbStyle); + } } function updateGridVerticalScrollbar(element: HTMLElement | null = gridScrollerElement()) { if (!element) { - hasGridVerticalOverflow.value = false; - gridVerticalScrollbarThumbTopPercent.value = 0; - gridVerticalScrollbarThumbHeightPercent.value = 100; + setGridVerticalOverflow(false); + gridVerticalScrollbarThumbTopPercent = 0; + gridVerticalScrollbarThumbHeightPercent = 100; + applyGridVerticalScrollbarThumbStyle(); return; } const maxScrollTop = Math.max(0, element.scrollHeight - element.clientHeight); - hasGridVerticalOverflow.value = maxScrollTop > 1; + setGridVerticalOverflow(maxScrollTop > 1); const rawThumbHeight = element.scrollHeight > 0 ? (element.clientHeight / element.scrollHeight) * 100 : 100; const thumbHeight = Math.min(100, Math.max(6, rawThumbHeight)); const thumbTravel = Math.max(0, 100 - thumbHeight); - gridVerticalScrollbarThumbHeightPercent.value = thumbHeight; - gridVerticalScrollbarThumbTopPercent.value = maxScrollTop > 0 ? (element.scrollTop / maxScrollTop) * thumbTravel : 0; + gridVerticalScrollbarThumbHeightPercent = thumbHeight; + gridVerticalScrollbarThumbTopPercent = maxScrollTop > 0 ? (element.scrollTop / maxScrollTop) * thumbTravel : 0; + if (!applyGridVerticalScrollbarThumbStyle() && hasGridVerticalOverflow.value) { + nextTick(applyGridVerticalScrollbarThumbStyle); + } +} + +function setGridHorizontalOverflow(overflow: boolean) { + if (hasGridHorizontalOverflow.value === overflow) return; + hasGridHorizontalOverflow.value = overflow; + if (overflow) nextTick(applyGridHorizontalScrollbarThumbStyle); +} + +function setGridVerticalOverflow(overflow: boolean) { + if (hasGridVerticalOverflow.value === overflow) return; + hasGridVerticalOverflow.value = overflow; + if (overflow) nextTick(applyGridVerticalScrollbarThumbStyle); +} + +function applyGridHorizontalScrollbarThumbStyle(): boolean { + const thumb = gridHorizontalScrollbarThumbRef.value; + if (!thumb) return false; + // Scroll thumb position changes on every drag frame; update it outside Vue's + // render path so large result grids do not re-render while the user drags. + thumb.style.width = `${gridHorizontalScrollbarThumbWidthPercent}%`; + thumb.style.left = `${gridHorizontalScrollbarThumbLeftPercent}%`; + return true; +} + +function applyGridVerticalScrollbarThumbStyle(): boolean { + const thumb = gridVerticalScrollbarThumbRef.value; + if (!thumb) return false; + thumb.style.height = `${gridVerticalScrollbarThumbHeightPercent}%`; + thumb.style.top = `${gridVerticalScrollbarThumbTopPercent}%`; + return true; +} + +function setGridHorizontalScrollbarDragging(dragging: boolean) { + gridHorizontalScrollbarTrackRef.value?.classList.toggle(GRID_HORIZONTAL_SCROLLBAR_DRAGGING_CLASS, dragging); +} + +function setGridVerticalScrollbarDragging(dragging: boolean) { + gridVerticalScrollbarTrackRef.value?.classList.toggle(GRID_VERTICAL_SCROLLBAR_DRAGGING_CLASS, dragging); } function scheduleGridHorizontalScrollbarUpdate() { @@ -2822,7 +2873,7 @@ function applyPendingGridHorizontalScrollbarDrag() { const dragState = gridHorizontalScrollbarDragState; if (!dragState) return; - const thumbWidthPx = dragState.trackRect.width * (gridHorizontalScrollbarThumbWidthPercent.value / 100); + const thumbWidthPx = dragState.trackRect.width * (gridHorizontalScrollbarThumbWidthPercent / 100); const maxThumbLeftPx = Math.max(1, dragState.trackRect.width - thumbWidthPx); const thumbLeftPx = Math.min(maxThumbLeftPx, Math.max(0, gridHorizontalScrollbarPendingClientX - dragState.trackRect.left - dragState.thumbOffsetPx)); const scroller = dragState.scroller; @@ -2859,7 +2910,7 @@ function stopGridHorizontalScrollbarDrag() { if (!gridHorizontalScrollbarDragState) return; flushGridHorizontalScrollbarDrag(); gridHorizontalScrollbarDragState = null; - gridHorizontalScrollbarDragging.value = false; + setGridHorizontalScrollbarDragging(false); window.removeEventListener("pointermove", onGridHorizontalScrollbarPointerMove, true); window.removeEventListener("pointerup", stopGridHorizontalScrollbarDrag, true); window.removeEventListener("pointercancel", stopGridHorizontalScrollbarDrag, true); @@ -2870,7 +2921,7 @@ function stopGridVerticalScrollbarDrag() { if (!gridVerticalScrollbarDragState) return; flushGridVerticalScrollbarDrag(); gridVerticalScrollbarDragState = null; - gridVerticalScrollbarDragging.value = false; + setGridVerticalScrollbarDragging(false); window.removeEventListener("pointermove", onGridVerticalScrollbarPointerMove, true); window.removeEventListener("pointerup", stopGridVerticalScrollbarDrag, true); window.removeEventListener("pointercancel", stopGridVerticalScrollbarDrag, true); @@ -2885,8 +2936,8 @@ function startGridHorizontalScrollbarDrag(event: PointerEvent) { 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); + const thumbLeftPx = trackRect.width * (gridHorizontalScrollbarThumbLeftPercent / 100); + const thumbWidthPx = trackRect.width * (gridHorizontalScrollbarThumbWidthPercent / 100); const pointerX = event.clientX - trackRect.left; const pointerInsideThumb = pointerX >= thumbLeftPx && pointerX <= thumbLeftPx + thumbWidthPx; @@ -2896,7 +2947,7 @@ function startGridHorizontalScrollbarDrag(event: PointerEvent) { thumbOffsetPx: pointerInsideThumb ? pointerX - thumbLeftPx : thumbWidthPx / 2, maxScrollLeft, }; - gridHorizontalScrollbarDragging.value = true; + setGridHorizontalScrollbarDragging(true); document.body.style.userSelect = "none"; window.addEventListener("pointermove", onGridHorizontalScrollbarPointerMove, true); window.addEventListener("pointerup", stopGridHorizontalScrollbarDrag, true); @@ -2910,7 +2961,7 @@ function applyPendingGridVerticalScrollbarDrag() { const dragState = gridVerticalScrollbarDragState; if (!dragState) return; - const thumbHeightPx = dragState.trackRect.height * (gridVerticalScrollbarThumbHeightPercent.value / 100); + const thumbHeightPx = dragState.trackRect.height * (gridVerticalScrollbarThumbHeightPercent / 100); const maxThumbTopPx = Math.max(1, dragState.trackRect.height - thumbHeightPx); const thumbTopPx = Math.min(maxThumbTopPx, Math.max(0, gridVerticalScrollbarPendingClientY - dragState.trackRect.top - dragState.thumbOffsetPx)); const scroller = dragState.scroller; @@ -2951,8 +3002,8 @@ function startGridVerticalScrollbarDrag(event: PointerEvent) { const maxScrollTop = Math.max(0, scroller.scrollHeight - scroller.clientHeight); if (maxScrollTop <= 1) return; const trackRect = track.getBoundingClientRect(); - const thumbTopPx = trackRect.height * (gridVerticalScrollbarThumbTopPercent.value / 100); - const thumbHeightPx = trackRect.height * (gridVerticalScrollbarThumbHeightPercent.value / 100); + const thumbTopPx = trackRect.height * (gridVerticalScrollbarThumbTopPercent / 100); + const thumbHeightPx = trackRect.height * (gridVerticalScrollbarThumbHeightPercent / 100); const pointerY = event.clientY - trackRect.top; const pointerInsideThumb = pointerY >= thumbTopPx && pointerY <= thumbTopPx + thumbHeightPx; @@ -2962,7 +3013,7 @@ function startGridVerticalScrollbarDrag(event: PointerEvent) { thumbOffsetPx: pointerInsideThumb ? pointerY - thumbTopPx : thumbHeightPx / 2, maxScrollTop, }; - gridVerticalScrollbarDragging.value = true; + setGridVerticalScrollbarDragging(true); document.body.style.userSelect = "none"; window.addEventListener("pointermove", onGridVerticalScrollbarPointerMove, true); window.addEventListener("pointerup", stopGridVerticalScrollbarDrag, true); @@ -2971,16 +3022,6 @@ function startGridVerticalScrollbarDrag(event: PointerEvent) { scheduleGridVerticalScrollbarDrag(event.clientY); } -const gridHorizontalScrollbarThumbStyle = computed(() => ({ - width: `${gridHorizontalScrollbarThumbWidthPercent.value}%`, - left: `${gridHorizontalScrollbarThumbLeftPercent.value}%`, -})); - -const gridVerticalScrollbarThumbStyle = computed(() => ({ - height: `${gridVerticalScrollbarThumbHeightPercent.value}%`, - top: `${gridVerticalScrollbarThumbTopPercent.value}%`, -})); - function updateGridScrollbarGutter(element: HTMLElement) { gridScrollbarGutter.value = scrollbarGutterWidth(element); } @@ -9913,11 +9954,11 @@ const gridContextMenuItems = computed(() => { {{ t("grid.loadingMore") }} -
-
+
+
-
-
+
+