fix(sidebar): locate active table reliably
This commit is contained in:
1 parent
33b4fc1dac
commit
07482557e6
3 files changed
+221
-14
No files matched your search
@@ -15,7 +15,7 @@ import { connectionPasteTargetGroupId, selectedConnectionClipboardNodes, selecte
|
||||
import { isEditableSidebarTypeSearchTarget, sidebarTypeSearchNextQuery } from "@/lib/sidebar/sidebarTypeSearch";
|
||||
import { usesTreeSchemaMode } from "@/lib/database/databaseFeatureSupport";
|
||||
import { connectionUsesDatabaseObjectTreeMode, effectiveDatabaseTypeForConnection } from "@/lib/database/jdbcDialect";
|
||||
import { activeTabSidebarTarget, findSidebarNodeForActiveTab, findSidebarNodeForTarget, findNodePathForTarget, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection, type ActiveTabSidebarTarget } from "@/lib/sidebar/sidebarActiveTabTarget";
|
||||
import { activeTabSidebarTarget, findSidebarNodeForActiveTab, findSidebarNodeForTarget, findNodePathForTarget, scrollTopForSidebarNode, shouldScrollActiveSidebarSelection, type ActiveTabSidebarTarget, type SidebarNodeScrollAlign } from "@/lib/sidebar/sidebarActiveTabTarget";
|
||||
import { findLoadedTableTargetForCandidate, queryContextTargetFromCandidate, queryCursorTableCandidate, type QueryCursorTableCandidate } from "@/lib/sql/queryCursorTableTarget";
|
||||
import { SIDEBAR_TREE_ROW_HEIGHT, SIDEBAR_TREE_PRERENDER_COUNT, SIDEBAR_TREE_SCROLL_BUFFER, flattenTree, shouldVirtualizeFlatTree, type FlatTreeNode } from "@/composables/useFlatTree";
|
||||
import { sidebarTreeContextKey } from "@/lib/sidebar/sidebarTreeContext";
|
||||
@@ -534,13 +534,33 @@ const pendingRenameGroupId = ref<string | null>(null);
|
||||
const highlightedNodeId = ref<string | null>(null);
|
||||
let highlightTimer: number | undefined;
|
||||
|
||||
// 等待虚拟列表渲染后再高亮。
|
||||
function waitForSidebarRenderFrame(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
window.requestAnimationFrame(() => resolve());
|
||||
});
|
||||
}
|
||||
|
||||
// 重新触发定位高亮,支持连续定位同一节点。
|
||||
async function flashSidebarNode(nodeId: string) {
|
||||
window.clearTimeout(highlightTimer);
|
||||
highlightedNodeId.value = null;
|
||||
await nextTick();
|
||||
await waitForSidebarRenderFrame();
|
||||
|
||||
highlightedNodeId.value = nodeId;
|
||||
highlightTimer = window.setTimeout(() => {
|
||||
if (highlightedNodeId.value === nodeId) highlightedNodeId.value = null;
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
function topOcclusionHeightForSidebarNode(nodeId: string): number {
|
||||
const sticky = stickyNode.value;
|
||||
if (!useVirtualTree.value || !sticky || sticky.id === nodeId) return 0;
|
||||
return SIDEBAR_TREE_ROW_HEIGHT;
|
||||
}
|
||||
|
||||
async function scrollToSidebarNode(nodeId: string) {
|
||||
async function scrollToSidebarNode(nodeId: string, options?: { align?: SidebarNodeScrollAlign }) {
|
||||
await nextTick();
|
||||
|
||||
const index = flatNodes.value.findIndex((item) => item.id === nodeId);
|
||||
@@ -552,6 +572,7 @@ async function scrollToSidebarNode(nodeId: string) {
|
||||
currentScrollTop: scroller.scrollTop,
|
||||
viewportHeight: scroller.clientHeight,
|
||||
topOcclusionHeight: topOcclusionHeightForSidebarNode(nodeId),
|
||||
...(options?.align ? { align: options.align } : {}),
|
||||
});
|
||||
if (nextScrollTop !== scroller.scrollTop) {
|
||||
scroller.scrollTop = nextScrollTop;
|
||||
@@ -661,13 +682,8 @@ async function locateActiveTabInSidebar() {
|
||||
store.treeSelectionAnchorId = match.id;
|
||||
await nextTick();
|
||||
|
||||
window.clearTimeout(highlightTimer);
|
||||
highlightedNodeId.value = match.id;
|
||||
highlightTimer = window.setTimeout(() => {
|
||||
highlightedNodeId.value = null;
|
||||
}, 1800);
|
||||
|
||||
await scrollToSidebarNode(match.id);
|
||||
await scrollToSidebarNode(match.id, { align: "smart" });
|
||||
await flashSidebarNode(match.id);
|
||||
}
|
||||
|
||||
function tableTargetFromCandidate(candidate: QueryCursorTableCandidate): ActiveTabSidebarTarget {
|
||||
|
||||
@@ -143,7 +143,13 @@ export function activeTabSidebarTarget(tab: QueryTab | undefined | null): Active
|
||||
|
||||
function schemaMatches(node: TreeNode, schema: string | undefined): boolean {
|
||||
if (!schema) return true;
|
||||
return (node.schema || "") === schema;
|
||||
|
||||
if ((node.schema || "") === schema) return true;
|
||||
// Some database engines (for example MySQL in database-object tree mode) do
|
||||
// not store a separate schema on table nodes. In that case the database name
|
||||
// is the effective schema, so a target schema equal to the node database
|
||||
// should still resolve to the loaded table node.
|
||||
return !node.schema && (node.database || "") === schema;
|
||||
}
|
||||
|
||||
export function matchesTarget(node: TreeNode, target: ActiveTabSidebarTarget): boolean {
|
||||
@@ -207,16 +213,28 @@ export function shouldScrollActiveSidebarSelection(options: { activeTabId: strin
|
||||
return options.activeTabId !== options.previousActiveTabId || (options.autoSelectEnabled && options.previousAutoSelectEnabled === false);
|
||||
}
|
||||
|
||||
export function scrollTopForSidebarNode(options: { index: number; currentScrollTop: number; viewportHeight: number; rowHeight?: number; topOcclusionHeight?: number }): number {
|
||||
// nearest is used for passive auto-selection; smart keeps context for explicit locate actions.
|
||||
export type SidebarNodeScrollAlign = "nearest" | "top" | "smart";
|
||||
|
||||
export function scrollTopForSidebarNode(options: { index: number; currentScrollTop: number; viewportHeight: number; rowHeight?: number; topOcclusionHeight?: number; align?: SidebarNodeScrollAlign }): number {
|
||||
const rowHeight = options.rowHeight ?? SIDEBAR_TREE_ROW_HEIGHT;
|
||||
if (options.index < 0 || options.viewportHeight <= 0) return options.currentScrollTop;
|
||||
|
||||
const rowTop = options.index * rowHeight;
|
||||
const rowBottom = rowTop + rowHeight;
|
||||
const viewportTop = options.currentScrollTop + (options.topOcclusionHeight ?? 0);
|
||||
const topOcclusionHeight = options.topOcclusionHeight ?? 0;
|
||||
if (options.align === "top") return Math.max(0, rowTop - topOcclusionHeight);
|
||||
if (options.align === "smart") {
|
||||
// Similar to IDE Locate: place the target around the upper third of the viewport.
|
||||
const availableViewportHeight = Math.max(rowHeight, options.viewportHeight - topOcclusionHeight);
|
||||
const smartOffset = Math.max(0, (availableViewportHeight - rowHeight) / 3);
|
||||
return Math.max(0, Math.round(rowTop - topOcclusionHeight - smartOffset));
|
||||
}
|
||||
|
||||
const viewportTop = options.currentScrollTop + topOcclusionHeight;
|
||||
const viewportBottom = options.currentScrollTop + options.viewportHeight;
|
||||
|
||||
if (rowTop < viewportTop) return Math.max(0, rowTop - (options.topOcclusionHeight ?? 0));
|
||||
if (rowTop < viewportTop) return Math.max(0, rowTop - topOcclusionHeight);
|
||||
if (rowBottom > viewportBottom) return Math.max(0, rowBottom - options.viewportHeight);
|
||||
return options.currentScrollTop;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user