fix(desktop): reduce sidebar and grid jank
This commit is contained in:
parent
1cfb39a7b0
commit
36daa214c3
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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[] {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<CustomContextMenu v-else :items="treeItemMenuItems()" v-slot="contextMenuSlot">
|
||||
<CustomContextMenu v-else :items="treeItemMenuItems" v-slot="contextMenuSlot">
|
||||
<div @contextmenu="onTreeItemContextMenu($event, contextMenuSlot.onContextMenu)">
|
||||
<LightTooltip :text="displayLabel(node)" :disabled="isTooltipDisabled()" side="right" :side-offset="8" :delay="0" :close-delay="0" :surface="detailTooltip ? 'popover' : 'foreground'">
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -17,8 +17,10 @@ export interface ContextMenuItem {
|
|||
children?: ContextMenuItem[];
|
||||
}
|
||||
|
||||
type ContextMenuItemsSource = 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<HTMLElement>();
|
||||
const activeItems = ref<ContextMenuItem[]>([]);
|
||||
|
||||
// Submenu state
|
||||
const activeSubIndex = ref<number | null>(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(() => {
|
|||
<!-- Main menu -->
|
||||
<Teleport to="body">
|
||||
<div v-if="show" ref="menuRef" :style="{ position: 'fixed', left: x + 'px', top: y + 'px', zIndex: 9999 }" class="bg-popover text-popover-foreground min-w-40 w-max max-w-[calc(100vw-16px)] rounded-[6px] p-1 overflow-y-auto ring-1 ring-foreground/10 shadow-lg">
|
||||
<template v-for="(item, index) in items" :key="index">
|
||||
<template v-for="(item, index) in activeItems" :key="index">
|
||||
<template v-if="item.visible !== false">
|
||||
<div v-if="item.separator" class="-mx-1 my-1 flex items-center px-1">
|
||||
<div class="h-px flex-1 bg-border/70" />
|
||||
|
|
@ -281,14 +288,14 @@ onBeforeUnmount(() => {
|
|||
<!-- Submenu -->
|
||||
<Teleport to="body">
|
||||
<div
|
||||
v-if="show && activeSubIndex !== null && items[activeSubIndex]?.children?.length"
|
||||
v-if="show && activeSubIndex !== null && activeItems[activeSubIndex]?.children?.length"
|
||||
ref="subRef"
|
||||
:style="{ position: 'fixed', left: subX + 'px', top: subY + 'px', zIndex: 10000, maxHeight: 'min(420px, calc(100vh - 16px))' }"
|
||||
class="bg-popover text-popover-foreground min-w-56 w-max max-w-[calc(100vw-16px)] rounded-[6px] p-1 overflow-y-auto ring-1 ring-foreground/10 shadow-lg"
|
||||
@mouseenter="onSubMouseEnter"
|
||||
@mouseleave="onSubMouseLeave"
|
||||
>
|
||||
<template v-for="(child, ci) in items[activeSubIndex]!.children!" :key="ci">
|
||||
<template v-for="(child, ci) in activeItems[activeSubIndex]!.children!" :key="ci">
|
||||
<template v-if="child.visible !== false">
|
||||
<div v-if="child.separator" class="-mx-1 my-1 flex items-center px-1">
|
||||
<div class="h-px flex-1 bg-border/70" />
|
||||
|
|
|
|||
Loading…
Reference in New Issue