feat(ui): route Ctrl+F search by focused surface
This commit is contained in:
23 files changed
+233
-56
No files matched your search
@@ -174,10 +174,13 @@ type AiAssistantHandle = {
|
||||
setPrompt: (text: string) => void;
|
||||
addTableMention: (target: { schema?: string; table: string }) => void;
|
||||
clearContextReferences: () => void;
|
||||
focusSearch: () => boolean;
|
||||
/** Opens a conversation by id (used by the background-run toast, §9). */
|
||||
selectConversationById: (conversationId: string) => void;
|
||||
};
|
||||
|
||||
type AuxiliarySearchSurface = "ai" | "history" | "sqlLibrary" | null;
|
||||
|
||||
const { t } = useI18n();
|
||||
const connectionStore = useConnectionStore();
|
||||
const queryStore = useQueryStore();
|
||||
@@ -365,6 +368,7 @@ const aiAssistantRef = ref<AiAssistantHandle | null>(null);
|
||||
const appSidebarRef = ref<InstanceType<typeof AppSidebar> | null>(null);
|
||||
const appTabBarRef = ref<InstanceType<typeof AppTabBar> | null>(null);
|
||||
const contentAreaRef = ref<InstanceType<typeof SqlEditorWorkspace> | null>(null);
|
||||
const lastFocusedAuxiliarySurface = ref<AuxiliarySearchSurface>(null);
|
||||
|
||||
const selectedSql = ref("");
|
||||
const cursorPos = ref(0);
|
||||
@@ -1428,6 +1432,9 @@ function setRightSidebarPanelOpen(panelId: RightSidebarPanelId, open: boolean) {
|
||||
applyRightSidebarPanelState(transitionRightSidebarPanels(currentRightSidebarPanelState(), panelId, open, exclusive));
|
||||
if (open) {
|
||||
lastOpenedRightSidebarPanel = panelId;
|
||||
if (panelId === "ai" || panelId === "history" || panelId === "sqlLibrary") {
|
||||
lastFocusedAuxiliarySurface.value = panelId;
|
||||
}
|
||||
} else if (lastOpenedRightSidebarPanel === panelId) {
|
||||
lastOpenedRightSidebarPanel = RIGHT_SIDEBAR_PANEL_IDS.find((candidate) => rightSidebarPanelRefs[candidate].value);
|
||||
}
|
||||
@@ -3148,6 +3155,14 @@ function handleTabSwitcherKeydownCapture(e: KeyboardEvent) {
|
||||
tabSwitcherKeyboard.handleKeydownCapture(e);
|
||||
}
|
||||
|
||||
function handleAuxiliarySearchKeydownCapture(e: KeyboardEvent) {
|
||||
if (e.defaultPrevented || !isFocusSearchShortcut(e, settingsStore.editorSettings.shortcuts)) return;
|
||||
const target = e.target instanceof Element ? e.target : document.activeElement instanceof Element ? document.activeElement : null;
|
||||
if (!focusSearchInAuxiliarySurface(target)) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function handleTabSwitcherWindowBlur() {
|
||||
tabSwitcherKeyboard.handleWindowBlur();
|
||||
}
|
||||
@@ -3180,6 +3195,50 @@ function handleNativeSelectAll(e: KeyboardEvent) {
|
||||
if (shouldBlockAppNativeSelectAll(e)) e.preventDefault();
|
||||
}
|
||||
|
||||
function focusSearchInput(selector: string): boolean {
|
||||
const input = Array.from(document.querySelectorAll<HTMLInputElement>(selector)).find((candidate) => candidate.getClientRects().length > 0);
|
||||
if (!input) return false;
|
||||
input.focus();
|
||||
input.select();
|
||||
return true;
|
||||
}
|
||||
|
||||
function focusSearchInAuxiliarySurface(target: Element | null): boolean {
|
||||
if (showConnectionDialog.value) {
|
||||
// The dialog is modal: keep the shortcut inside it and never focus a
|
||||
// surface hidden behind the overlay, even when its search input is absent.
|
||||
focusSearchInput("[data-connection-db-search]");
|
||||
return true;
|
||||
}
|
||||
if (showSettingsPage.value) return focusSearchInput("[data-settings-global-search]");
|
||||
if (showDriverStore.value) {
|
||||
const selector = driverStoreActiveTab.value === "jdbc" ? "[data-driver-store-jdbc-search]" : "[data-driver-store-agent-search]";
|
||||
return focusSearchInput(selector);
|
||||
}
|
||||
if (showPluginCenter.value) return focusSearchInput("[data-plugin-marketplace-search]");
|
||||
|
||||
if (showAiPanel.value && target?.closest("[data-ai-assistant-root], [data-ai-conversation-search]")) {
|
||||
if (aiAssistantRef.value) return aiAssistantRef.value.focusSearch();
|
||||
invokeWhenAiReady((handle) => handle.focusSearch());
|
||||
return true;
|
||||
}
|
||||
if (showHistory.value && target?.closest("[data-history-panel], [data-history-search]")) return focusSearchInput("[data-history-search]");
|
||||
if (showSqlLibraryPanel.value && target?.closest("[data-sql-library-panel], [data-sql-library-search]")) return focusSearchInput("[data-sql-library-search]");
|
||||
|
||||
if (lastFocusedAuxiliarySurface.value === "ai" && showAiPanel.value) {
|
||||
if (aiAssistantRef.value) return aiAssistantRef.value.focusSearch();
|
||||
invokeWhenAiReady((handle) => handle.focusSearch());
|
||||
return true;
|
||||
}
|
||||
if (lastFocusedAuxiliarySurface.value === "history" && showHistory.value) return focusSearchInput("[data-history-search]");
|
||||
if (lastFocusedAuxiliarySurface.value === "sqlLibrary" && showSqlLibraryPanel.value) return focusSearchInput("[data-sql-library-search]");
|
||||
return false;
|
||||
}
|
||||
|
||||
function rememberAuxiliarySearchSurface(surface: Exclude<AuxiliarySearchSurface, null>) {
|
||||
lastFocusedAuxiliarySurface.value = surface;
|
||||
}
|
||||
|
||||
function setPluginWorkbenchTabRef(tabId: string, element: unknown) {
|
||||
if (element && typeof element === "object" && "refresh" in element && typeof element.refresh === "function") {
|
||||
pluginWorkbenchTabRefs.set(tabId, element as PluginWorkbenchTabHandle);
|
||||
@@ -3252,9 +3311,11 @@ async function handleKeydown(e: KeyboardEvent) {
|
||||
return;
|
||||
}
|
||||
if (isFocusSearchShortcut(e, shortcuts)) {
|
||||
// Route by the event target so focus inside the shared result grid (or a
|
||||
// Teleported cell-detail dialog) lands on the result search, not the editor.
|
||||
const focused = contentAreaRef.value?.focusSearch(e.target instanceof Element ? e.target : null) || appSidebarRef.value?.focusSearch();
|
||||
// Keep the focused navigation surface ahead of the active content tab.
|
||||
// Otherwise Ctrl+F from empty space in the sidebar incorrectly opens the
|
||||
// search belonging to the active table/query tab.
|
||||
const target = e.target instanceof Element ? e.target : null;
|
||||
const focused = target?.closest("[data-app-sidebar]") ? appSidebarRef.value?.focusSearch(target) : focusSearchInAuxiliarySurface(target) || contentAreaRef.value?.focusSearch(target) || appSidebarRef.value?.focusSearch(target);
|
||||
if (focused) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
@@ -3547,6 +3608,7 @@ onMounted(async () => {
|
||||
void applyUiScale(settingsStore.editorSettings.uiScale);
|
||||
window.addEventListener("keydown", handleNativeSelectAll, true);
|
||||
window.addEventListener("keydown", handleTabSwitcherKeydownCapture, true);
|
||||
window.addEventListener("keydown", handleAuxiliarySearchKeydownCapture, true);
|
||||
window.addEventListener("keydown", handleKeydown);
|
||||
window.addEventListener("keyup", handleKeyup, true);
|
||||
window.addEventListener("blur", handleTabSwitcherWindowBlur);
|
||||
@@ -3628,6 +3690,7 @@ onUnmounted(() => {
|
||||
}
|
||||
window.removeEventListener("keydown", handleNativeSelectAll, true);
|
||||
window.removeEventListener("keydown", handleTabSwitcherKeydownCapture, true);
|
||||
window.removeEventListener("keydown", handleAuxiliarySearchKeydownCapture, true);
|
||||
window.removeEventListener("keydown", handleKeydown);
|
||||
window.removeEventListener("keyup", handleKeyup, true);
|
||||
window.removeEventListener("blur", handleTabSwitcherWindowBlur);
|
||||
@@ -3958,7 +4021,7 @@ onUnmounted(() => {
|
||||
:style="isAiPanelMaximized ? {} : { width: aiPanelWidth + 'px' }"
|
||||
>
|
||||
<div v-if="!isAiPanelMaximized" class="panel-resize-handle panel-resize-handle--left" @pointerdown="startAiPanelResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]" @mousedown="rememberAuxiliarySearchSurface('ai')">
|
||||
<AiAssistant
|
||||
v-if="aiPanelReady"
|
||||
ref="aiAssistantRef"
|
||||
@@ -3985,8 +4048,10 @@ onUnmounted(() => {
|
||||
:style="{ width: historyWidth + 'px' }"
|
||||
>
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startHistoryResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
|
||||
<QueryHistory :current-connection-id="activeTab?.connectionId" :current-database="activeTab?.database" @restore="restoreHistorySql" @analyze-ai="analyzeHistoryWithAi" @close="closeRightSidebarPanel('history')" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]" @mousedown="rememberAuxiliarySearchSurface('history')">
|
||||
<div data-history-panel class="h-full min-h-0">
|
||||
<QueryHistory :current-connection-id="activeTab?.connectionId" :current-database="activeTab?.database" @restore="restoreHistorySql" @analyze-ai="analyzeHistoryWithAi" @close="closeRightSidebarPanel('history')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -3997,8 +4062,10 @@ onUnmounted(() => {
|
||||
:style="{ width: sqlLibraryWidth + 'px' }"
|
||||
>
|
||||
<div class="panel-resize-handle panel-resize-handle--left" @pointerdown="startSqlLibraryResize" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]">
|
||||
<SqlLibraryPanel @close="closeRightSidebarPanel('sqlLibrary')" />
|
||||
<div class="h-full min-h-0 overflow-hidden rounded-[inherit]" @mousedown="rememberAuxiliarySearchSurface('sqlLibrary')">
|
||||
<div data-sql-library-panel class="h-full min-h-0">
|
||||
<SqlLibraryPanel @close="closeRightSidebarPanel('sqlLibrary')" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in new issue
Block a user