feat(grid): add DDL search navigation
This commit is contained in:
parent
438164dd64
commit
c31d403af3
|
|
@ -8,6 +8,7 @@ import {
|
|||
Upload,
|
||||
Trash2,
|
||||
ChevronDown,
|
||||
ChevronUp,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Search,
|
||||
|
|
@ -6690,6 +6691,57 @@ const showTableInfo = ref(false);
|
|||
const activeTableInfoTab = ref<TableInfoTab>(settingsStore.editorSettings.tableInfoActiveTab);
|
||||
const ddlContent = ref("");
|
||||
const ddlPreRef = ref<HTMLPreElement | null>(null);
|
||||
const ddlSearchMatchCount = ref(0);
|
||||
const ddlSearchMatchIndex = ref(0);
|
||||
|
||||
function scrollDdlSearchMatchIntoView(match: HTMLElement) {
|
||||
const pre = ddlPreRef.value;
|
||||
if (!pre) return;
|
||||
|
||||
const preRect = pre.getBoundingClientRect();
|
||||
const matchRect = match.getBoundingClientRect();
|
||||
pre.scrollTop += matchRect.top - preRect.top - (pre.clientHeight - matchRect.height) / 2;
|
||||
pre.scrollLeft += matchRect.left - preRect.left - (pre.clientWidth - matchRect.width) / 2;
|
||||
}
|
||||
|
||||
function syncDdlSearchMatches(scrollToActive = false) {
|
||||
const pre = ddlPreRef.value;
|
||||
if (!pre || activeTableInfoTab.value !== "ddl" || !searchQuery.value) {
|
||||
ddlSearchMatchCount.value = 0;
|
||||
ddlSearchMatchIndex.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
const matches = Array.from(pre.querySelectorAll<HTMLElement>("mark.ddl-search-match"));
|
||||
ddlSearchMatchCount.value = matches.length;
|
||||
if (matches.length === 0) {
|
||||
ddlSearchMatchIndex.value = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
ddlSearchMatchIndex.value = Math.min(ddlSearchMatchIndex.value, matches.length - 1);
|
||||
matches.forEach((match, index) => match.classList.toggle("ddl-search-match-active", index === ddlSearchMatchIndex.value));
|
||||
const activeMatch = matches[ddlSearchMatchIndex.value];
|
||||
if (scrollToActive && activeMatch) scrollDdlSearchMatchIntoView(activeMatch);
|
||||
}
|
||||
|
||||
function navigateDdlSearch(delta: -1 | 1) {
|
||||
const count = ddlSearchMatchCount.value;
|
||||
if (count === 0) return;
|
||||
ddlSearchMatchIndex.value = (ddlSearchMatchIndex.value + delta + count) % count;
|
||||
syncDdlSearchMatches(true);
|
||||
}
|
||||
|
||||
function onTableInfoSearchKeydown(e: KeyboardEvent) {
|
||||
if (e.key === "Escape") {
|
||||
searchQuery.value = "";
|
||||
return;
|
||||
}
|
||||
if (e.key !== "Enter" || activeTableInfoTab.value !== "ddl") return;
|
||||
e.preventDefault();
|
||||
navigateDdlSearch(e.shiftKey ? -1 : 1);
|
||||
}
|
||||
|
||||
function onDdlKeydown(e: KeyboardEvent) {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === "a") {
|
||||
e.preventDefault();
|
||||
|
|
@ -6745,11 +6797,16 @@ watch(activeTableInfoTab, () => {
|
|||
});
|
||||
|
||||
watch([activeTableInfoTab, ddlLoading], ([tab, loading]) => {
|
||||
if (tab === "ddl" && !loading) {
|
||||
void nextTick(() => {
|
||||
ddlPreRef.value?.focus();
|
||||
});
|
||||
if (tab !== "ddl") return;
|
||||
if (loading) {
|
||||
ddlSearchMatchCount.value = 0;
|
||||
ddlSearchMatchIndex.value = 0;
|
||||
return;
|
||||
}
|
||||
void nextTick(() => {
|
||||
ddlPreRef.value?.focus();
|
||||
syncDdlSearchMatches(true);
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
|
|
@ -7216,10 +7273,20 @@ const filteredDdlContent = computed(() => {
|
|||
const regex = new RegExp(`(${escaped})`, "gi");
|
||||
// Match only text between > and < (text nodes), then replace the search term within those spans
|
||||
return html.replace(/>([^<]*)</g, (_, text) => {
|
||||
return `>${text.replace(regex, "<mark>$1</mark>")}<`;
|
||||
return `>${text.replace(regex, '<mark class="ddl-search-match">$1</mark>')}<`;
|
||||
});
|
||||
});
|
||||
|
||||
watch(
|
||||
[filteredDdlContent, searchQuery],
|
||||
async () => {
|
||||
ddlSearchMatchIndex.value = 0;
|
||||
await nextTick();
|
||||
syncDdlSearchMatches(true);
|
||||
},
|
||||
{ flush: "post" },
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
useTransaction,
|
||||
transactionActive,
|
||||
|
|
@ -8616,12 +8683,25 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
</div>
|
||||
|
||||
<div class="px-2 py-1.5 border-b shrink-0 bg-background">
|
||||
<div class="relative">
|
||||
<Search class="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
|
||||
<input v-model="searchQuery" :placeholder="t('grid.tableInfoSearch')" class="w-full h-7 pl-7 pr-6 text-xs bg-muted/50 rounded border border-border focus:outline-none focus:border-primary/50" @keydown.escape="searchQuery = ''" />
|
||||
<button v-if="searchQuery" class="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" @click="searchQuery = ''">
|
||||
<X class="w-3 h-3" />
|
||||
</button>
|
||||
<div class="flex min-w-0 items-center gap-1">
|
||||
<div class="relative min-w-0 flex-1">
|
||||
<Search class="absolute left-2 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground" />
|
||||
<input v-model="searchQuery" :placeholder="t('grid.tableInfoSearch')" class="w-full h-7 pl-7 pr-6 text-xs bg-muted/50 rounded border border-border focus:outline-none focus:border-primary/50" @keydown="onTableInfoSearchKeydown" />
|
||||
<button v-if="searchQuery" type="button" class="absolute right-1.5 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground" @click="searchQuery = ''">
|
||||
<X class="w-3 h-3" />
|
||||
</button>
|
||||
</div>
|
||||
<template v-if="activeTableInfoTab === 'ddl'">
|
||||
<span class="w-9 shrink-0 text-center text-[11px] tabular-nums text-muted-foreground">
|
||||
{{ searchQuery ? (ddlSearchMatchCount > 0 ? `${ddlSearchMatchIndex + 1}/${ddlSearchMatchCount}` : "0") : "" }}
|
||||
</span>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" :title="t('editor.search.prevMatch')" :aria-label="t('editor.search.prevMatch')" :disabled="ddlSearchMatchCount === 0" @click="navigateDdlSearch(-1)">
|
||||
<ChevronUp class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7 shrink-0" :title="t('editor.search.nextMatch')" :aria-label="t('editor.search.nextMatch')" :disabled="ddlSearchMatchCount === 0" @click="navigateDdlSearch(1)">
|
||||
<ChevronDown class="h-3.5 w-3.5" />
|
||||
</Button>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
@ -9725,4 +9805,16 @@ const gridContextMenuItems = computed<ContextMenuItem[]>(() => {
|
|||
color: rgb(213 111 44);
|
||||
color: oklch(0.65 0.15 50);
|
||||
}
|
||||
|
||||
.ddl-code :deep(.ddl-search-match) {
|
||||
border-radius: 2px;
|
||||
background: var(--data-grid-cell-search-bg);
|
||||
color: inherit;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ddl-code :deep(.ddl-search-match-active) {
|
||||
background: var(--data-grid-cell-current-search-bg);
|
||||
outline: 1px solid var(--data-grid-cell-current-search-border);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
import { readFileSync } from "node:fs";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const dataGridSource = readFileSync(new URL("../DataGrid.vue", import.meta.url), "utf8");
|
||||
|
||||
describe("DataGrid DDL search navigation", () => {
|
||||
it("resets navigation when the raw search query changes", () => {
|
||||
expect(dataGridSource).toMatch(/watch\(\s*\[filteredDdlContent, searchQuery\],/);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue