fix(desktop): show horizontal data grid scrollbar (#1018)
This commit is contained in:
1 parent
9793dc94b7
commit
215b0cf7d4
1 file changed
+196
-2
@@ -18,7 +18,7 @@ const structuredFilterStateCache = new Map<string, StructuredFilterCacheState>()
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, nextTick, onMounted, onUnmounted, onActivated, onDeactivated, useSlots, watch, defineAsyncComponent, type Component } from "vue";
|
||||
import { computed, nextTick, onMounted, onUnmounted, onActivated, onDeactivated, useSlots, watch, defineAsyncComponent, type Component, type CSSProperties } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import {
|
||||
ArrowUp,
|
||||
@@ -1393,6 +1393,17 @@ const rowStatusFilter = ref<RowStatusFilter>("all");
|
||||
const gridRef = ref<HTMLDivElement>();
|
||||
const headerRef = ref<HTMLDivElement>();
|
||||
const gridScrollbarGutter = ref(0);
|
||||
const gridHorizontalScrollbarTrackRef = ref<HTMLDivElement>();
|
||||
const hasGridHorizontalOverflow = ref(false);
|
||||
const gridHorizontalScrollbarThumbLeftPercent = ref(0);
|
||||
const gridHorizontalScrollbarThumbWidthPercent = ref(100);
|
||||
const gridHorizontalScrollbarDragging = ref(false);
|
||||
let gridHorizontalScrollbarFrame = 0;
|
||||
let gridHorizontalScrollbarResizeObserver: ResizeObserver | null = null;
|
||||
let gridHorizontalScrollbarDragState: {
|
||||
trackRect: DOMRect;
|
||||
thumbOffsetPx: number;
|
||||
} | null = null;
|
||||
const hiddenColumnIndexes = ref<Set<number>>(new Set());
|
||||
const nullColumnsHidden = ref(false);
|
||||
const autoHiddenNullColumnIndexes = ref<Set<number>>(new Set());
|
||||
@@ -1549,8 +1560,122 @@ const renderedColumnOffsets = computed(() => {
|
||||
function updateGridHorizontalViewport(element: HTMLElement) {
|
||||
gridHorizontalScrollLeft.value = element.scrollLeft;
|
||||
gridViewportWidth.value = element.clientWidth;
|
||||
updateGridHorizontalScrollbar(element);
|
||||
}
|
||||
|
||||
function gridScrollerElement(): HTMLElement | null {
|
||||
return gridRef.value?.querySelector<HTMLElement>(".data-grid-scroller") ?? null;
|
||||
}
|
||||
|
||||
function updateGridHorizontalScrollbar(element: HTMLElement | null = gridScrollerElement()) {
|
||||
if (!element) {
|
||||
hasGridHorizontalOverflow.value = false;
|
||||
gridHorizontalScrollbarThumbLeftPercent.value = 0;
|
||||
gridHorizontalScrollbarThumbWidthPercent.value = 100;
|
||||
return;
|
||||
}
|
||||
|
||||
const maxScrollLeft = Math.max(0, element.scrollWidth - element.clientWidth);
|
||||
hasGridHorizontalOverflow.value = 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;
|
||||
}
|
||||
|
||||
function scheduleGridHorizontalScrollbarUpdate() {
|
||||
if (gridHorizontalScrollbarFrame) return;
|
||||
const update = () => {
|
||||
gridHorizontalScrollbarFrame = 0;
|
||||
updateGridHorizontalScrollbar();
|
||||
};
|
||||
if (typeof requestAnimationFrame === "function") {
|
||||
gridHorizontalScrollbarFrame = requestAnimationFrame(update);
|
||||
} else {
|
||||
window.setTimeout(update, 0);
|
||||
}
|
||||
}
|
||||
|
||||
function observeGridHorizontalScrollbarScroller() {
|
||||
gridHorizontalScrollbarResizeObserver?.disconnect();
|
||||
gridHorizontalScrollbarResizeObserver = null;
|
||||
const scroller = gridScrollerElement();
|
||||
if (scroller && typeof ResizeObserver !== "undefined") {
|
||||
gridHorizontalScrollbarResizeObserver = new ResizeObserver(scheduleGridHorizontalScrollbarUpdate);
|
||||
gridHorizontalScrollbarResizeObserver.observe(scroller);
|
||||
for (const child of Array.from(scroller.children)) {
|
||||
gridHorizontalScrollbarResizeObserver.observe(child);
|
||||
}
|
||||
}
|
||||
scheduleGridHorizontalScrollbarUpdate();
|
||||
}
|
||||
|
||||
function applyGridHorizontalScrollbarDrag(clientX: number) {
|
||||
const scroller = gridScrollerElement();
|
||||
const dragState = gridHorizontalScrollbarDragState;
|
||||
if (!scroller || !dragState) return;
|
||||
|
||||
const maxScrollLeft = Math.max(0, scroller.scrollWidth - scroller.clientWidth);
|
||||
if (maxScrollLeft <= 1) 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;
|
||||
updateGridHorizontalViewport(scroller);
|
||||
if (headerRef.value) headerRef.value.scrollLeft = scroller.scrollLeft;
|
||||
if (useCanvasGridRows.value) {
|
||||
syncCanvasViewport();
|
||||
}
|
||||
}
|
||||
|
||||
function onGridHorizontalScrollbarPointerMove(event: PointerEvent) {
|
||||
if (!gridHorizontalScrollbarDragState) return;
|
||||
event.preventDefault();
|
||||
applyGridHorizontalScrollbarDrag(event.clientX);
|
||||
}
|
||||
|
||||
function stopGridHorizontalScrollbarDrag() {
|
||||
if (!gridHorizontalScrollbarDragState) return;
|
||||
gridHorizontalScrollbarDragState = null;
|
||||
gridHorizontalScrollbarDragging.value = false;
|
||||
window.removeEventListener("pointermove", onGridHorizontalScrollbarPointerMove, true);
|
||||
window.removeEventListener("pointerup", stopGridHorizontalScrollbarDrag, true);
|
||||
window.removeEventListener("pointercancel", stopGridHorizontalScrollbarDrag, true);
|
||||
document.body.style.userSelect = "";
|
||||
}
|
||||
|
||||
function startGridHorizontalScrollbarDrag(event: PointerEvent) {
|
||||
const scroller = gridScrollerElement();
|
||||
const track = gridHorizontalScrollbarTrackRef.value;
|
||||
if (!scroller || !track || !hasGridHorizontalOverflow.value) return;
|
||||
|
||||
const trackRect = track.getBoundingClientRect();
|
||||
const thumbLeftPx = trackRect.width * (gridHorizontalScrollbarThumbLeftPercent.value / 100);
|
||||
const thumbWidthPx = trackRect.width * (gridHorizontalScrollbarThumbWidthPercent.value / 100);
|
||||
const pointerX = event.clientX - trackRect.left;
|
||||
const pointerInsideThumb = pointerX >= thumbLeftPx && pointerX <= thumbLeftPx + thumbWidthPx;
|
||||
|
||||
gridHorizontalScrollbarDragState = {
|
||||
trackRect,
|
||||
thumbOffsetPx: pointerInsideThumb ? pointerX - thumbLeftPx : thumbWidthPx / 2,
|
||||
};
|
||||
gridHorizontalScrollbarDragging.value = true;
|
||||
document.body.style.userSelect = "none";
|
||||
window.addEventListener("pointermove", onGridHorizontalScrollbarPointerMove, true);
|
||||
window.addEventListener("pointerup", stopGridHorizontalScrollbarDrag, true);
|
||||
window.addEventListener("pointercancel", stopGridHorizontalScrollbarDrag, true);
|
||||
event.preventDefault();
|
||||
applyGridHorizontalScrollbarDrag(event.clientX);
|
||||
}
|
||||
|
||||
const gridHorizontalScrollbarThumbStyle = computed<CSSProperties>(() => ({
|
||||
width: `${gridHorizontalScrollbarThumbWidthPercent.value}%`,
|
||||
left: `${gridHorizontalScrollbarThumbLeftPercent.value}%`,
|
||||
}));
|
||||
|
||||
function updateGridScrollbarGutter(element: HTMLElement) {
|
||||
gridScrollbarGutter.value = scrollbarGutterWidth(element);
|
||||
}
|
||||
@@ -1563,6 +1688,7 @@ function refreshGridScrollerMetrics() {
|
||||
if (headerRef.value) {
|
||||
headerRef.value.scrollLeft = scrollerEl.scrollLeft;
|
||||
}
|
||||
observeGridHorizontalScrollbarScroller();
|
||||
}
|
||||
|
||||
function syncHeaderScroll(e: Event) {
|
||||
@@ -3281,6 +3407,17 @@ let canvasPixelRatioFrame = 0;
|
||||
let canvasDrawFrame = 0;
|
||||
let dataGridIsActive = true;
|
||||
|
||||
watch(
|
||||
() => [totalWidth.value, displayRowCount.value, useCanvasGridRows.value, props.result.columns.join("\u0000")],
|
||||
() => {
|
||||
nextTick(() => {
|
||||
refreshGridScrollerMetrics();
|
||||
observeGridHorizontalScrollbarScroller();
|
||||
});
|
||||
},
|
||||
{ flush: "post" },
|
||||
);
|
||||
|
||||
function toggleDataGridRenderMode() {
|
||||
settingsStore.updateEditorSettings({
|
||||
dataGridRenderMode: dataGridRenderMode.value === "canvas" ? "dom" : "canvas",
|
||||
@@ -3727,7 +3864,11 @@ function pauseCanvasGridWork() {
|
||||
|
||||
function resumeCanvasGridWork() {
|
||||
dataGridIsActive = true;
|
||||
nextTick(attachCanvasResizeObserver);
|
||||
nextTick(() => {
|
||||
attachCanvasResizeObserver();
|
||||
refreshGridScrollerMetrics();
|
||||
observeGridHorizontalScrollbarScroller();
|
||||
});
|
||||
}
|
||||
|
||||
onMounted(resumeCanvasGridWork);
|
||||
@@ -3741,6 +3882,11 @@ onMounted(() => {
|
||||
onDeactivated(pauseCanvasGridWork);
|
||||
onUnmounted(() => {
|
||||
pauseCanvasGridWork();
|
||||
gridHorizontalScrollbarResizeObserver?.disconnect();
|
||||
stopGridHorizontalScrollbarDrag();
|
||||
if (gridHorizontalScrollbarFrame && typeof cancelAnimationFrame === "function") {
|
||||
cancelAnimationFrame(gridHorizontalScrollbarFrame);
|
||||
}
|
||||
if (typeof window === "undefined") return;
|
||||
window.removeEventListener("resize", scheduleCanvasPixelRatioRefresh);
|
||||
window.visualViewport?.removeEventListener("resize", scheduleCanvasPixelRatioRefresh);
|
||||
@@ -6679,6 +6825,9 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
</div>
|
||||
</template>
|
||||
</RecycleScroller>
|
||||
<div v-if="hasGridHorizontalOverflow" ref="gridHorizontalScrollbarTrackRef" class="data-grid-horizontal-scrollbar" :class="{ 'data-grid-horizontal-scrollbar--dragging': gridHorizontalScrollbarDragging }" @pointerdown="startGridHorizontalScrollbarDrag">
|
||||
<div class="data-grid-horizontal-scrollbar__thumb" :style="gridHorizontalScrollbarThumbStyle" />
|
||||
</div>
|
||||
<div v-if="loading" class="absolute inset-0 z-20 bg-background/50 flex items-center justify-center">
|
||||
<div class="flex items-center gap-2 px-3 py-1.5 rounded-md bg-background border shadow-sm text-xs text-muted-foreground">
|
||||
<Loader2 class="w-3.5 h-3.5 animate-spin" />
|
||||
@@ -7529,6 +7678,11 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
scrollbar-gutter: stable;
|
||||
will-change: scroll-position;
|
||||
contain: layout style paint;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
.data-grid-scroller::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.data-grid-scroller :deep(.vue-recycle-scroller__item-wrapper) {
|
||||
@@ -7544,6 +7698,46 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.data-grid-horizontal-scrollbar {
|
||||
position: absolute;
|
||||
inset-inline: calc(var(--row-num-w) + 8px) 10px;
|
||||
bottom: 8px;
|
||||
z-index: 30;
|
||||
height: 10px;
|
||||
cursor: pointer;
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
.data-grid-horizontal-scrollbar::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset-inline: 0;
|
||||
top: 4px;
|
||||
height: 2px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklch, var(--foreground) 12%, transparent);
|
||||
}
|
||||
|
||||
.data-grid-horizontal-scrollbar__thumb {
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
height: 4px;
|
||||
min-width: 24px;
|
||||
border-radius: 999px;
|
||||
background: color-mix(in oklch, var(--foreground) 42%, transparent);
|
||||
transition:
|
||||
height 120ms ease,
|
||||
background-color 120ms ease,
|
||||
top 120ms ease;
|
||||
}
|
||||
|
||||
.data-grid-horizontal-scrollbar:hover .data-grid-horizontal-scrollbar__thumb,
|
||||
.data-grid-horizontal-scrollbar--dragging .data-grid-horizontal-scrollbar__thumb {
|
||||
top: 2px;
|
||||
height: 6px;
|
||||
background: color-mix(in oklch, var(--foreground) 62%, transparent);
|
||||
}
|
||||
|
||||
.canvas-grid-surface {
|
||||
cursor: cell;
|
||||
font-family: inherit;
|
||||
|
||||
Reference in new issue
Block a user