From 45558192671f48ca3d8a0b3633094decaf5e5813 Mon Sep 17 00:00:00 2001 From: bread <136384195+bbbreaddd@users.noreply.github.com> Date: Fri, 13 Feb 2026 02:56:28 -0800 Subject: [PATCH] Improve search (#4754) * Fix mergenearbymarks * improve search arrow-key navigation * disable dark search results on mobile * stop warning --- docs/.vitepress/config.mts | 7 ++ .../theme/components/VPLocalSearchBox.vue | 101 +++++++++++------- 2 files changed, 71 insertions(+), 37 deletions(-) diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index f5a7cb5ec..5e202fc98 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -89,6 +89,13 @@ export default defineConfig({ .finally(() => consola.success('Success!')) }, vite: { + css: { + preprocessorOptions: { + scss: { + api: 'modern-compiler' + } + } + }, ssr: { noExternal: ['@fmhy/components'] }, diff --git a/docs/.vitepress/theme/components/VPLocalSearchBox.vue b/docs/.vitepress/theme/components/VPLocalSearchBox.vue index 084549d0e..71a54be23 100644 --- a/docs/.vitepress/theme/components/VPLocalSearchBox.vue +++ b/docs/.vitepress/theme/components/VPLocalSearchBox.vue @@ -420,6 +420,12 @@ async function mergeNearbyMarks() { while (i < marks.length - 1) { const currentMark = marks[i] const nextMark = marks[i + 1] + + // Ensure they are siblings to safely merge + if (currentMark.parentNode !== nextMark.parentNode) { + i++ + continue + } // Calculate distance between marks const currentEnd = currentMark.offsetLeft + currentMark.offsetWidth @@ -431,8 +437,17 @@ async function mergeNearbyMarks() { // Merge if they're close (within 20px) and on the same line if (distance >= 0 && distance < 20 && onSameLine) { - // Create a merged mark element - const textBetween = getTextBetweenMarks(currentMark, nextMark) + // Collect text between and remove intermediate nodes + let textBetween = '' + let node = currentMark.nextSibling + + while (node && node !== nextMark) { + textBetween += node.textContent || '' + const next = node.nextSibling + node.parentNode?.removeChild(node) + node = next + } + const mergedText = currentMark.textContent + textBetween + nextMark.textContent currentMark.textContent = mergedText @@ -446,27 +461,6 @@ async function mergeNearbyMarks() { } } -/** - * Extracts the plain text content between two mark elements. - * Used when merging adjacent highlights to preserve the spacing/text between them. - */ -function getTextBetweenMarks(mark1: HTMLElement, mark2: HTMLElement): string { - const parent = mark1.parentNode - if (!parent) return '' - - let text = '' - let node: Node | null = mark1.nextSibling - - while (node && node !== mark2) { - if (node.nodeType === Node.TEXT_NODE) { - text += node.textContent || '' - } - node = node.nextSibling - } - - return text -} - /** * Custom feature: Navigate to the next highlighted match in the current result. * Cycles back to the first match when reaching the end. @@ -644,6 +638,18 @@ function scrollToSelectedResult() { onKeyStroke('ArrowUp', (event) => { event.preventDefault() + + if (resultsEl.value && document.activeElement === resultsEl.value && selectedIndex.value === 0) { + selectedIndex.value = -1 + searchInput.value?.focus() + return + } + + if (resultsEl.value && document.activeElement === searchInput.value) { + resultsEl.value.focus() + // Fall through to wrap to bottom + } + selectedIndex.value-- if (selectedIndex.value < 0) { selectedIndex.value = results.value.length - 1 @@ -654,6 +660,12 @@ onKeyStroke('ArrowUp', (event) => { onKeyStroke('ArrowDown', (event) => { event.preventDefault() + + if (resultsEl.value && document.activeElement === searchInput.value) { + resultsEl.value.focus() + // Fall through to select first item (from -1 to 0) + } + selectedIndex.value++ if (selectedIndex.value >= results.value.length) { selectedIndex.value = 0 @@ -693,29 +705,37 @@ onKeyStroke('Escape', () => { */ onKeyStroke('ArrowLeft', (event) => { // Navigate to previous match - only when viewing detailed excerpts with highlights - if (showDetailedList.value && selectedIndex.value >= 0 && (resultMarks.value.get(selectedIndex.value)?.length ?? 0) > 0) { - if (event.target === searchInput.value) { - if (event.shiftKey) return - const { selectionStart, selectionEnd } = searchInput.value! - // Only hijack if cursor is collapsed at the start - if (selectionStart !== 0 || selectionEnd !== 0) return + const targetIndex = selectedIndex.value === -1 ? 0 : selectedIndex.value + if (showDetailedList.value && (resultMarks.value.get(targetIndex)?.length ?? 0) > 0) { + if (document.activeElement === searchInput.value) { + // Only hijack if modifier is pressed + if (!event.altKey && !event.ctrlKey) return } event.preventDefault() - prevMatch(selectedIndex.value) + prevMatch(targetIndex) } }) onKeyStroke('ArrowRight', (event) => { // Navigate to next match - only when viewing detailed excerpts with highlights - if (showDetailedList.value && selectedIndex.value >= 0 && (resultMarks.value.get(selectedIndex.value)?.length ?? 0) > 0) { - if (event.target === searchInput.value) { + const targetIndex = selectedIndex.value === -1 ? 0 : selectedIndex.value + if (showDetailedList.value && (resultMarks.value.get(targetIndex)?.length ?? 0) > 0) { + if (document.activeElement === searchInput.value) { if (event.shiftKey) return - const { selectionStart, selectionEnd, value } = searchInput.value! - // Only hijack if cursor is collapsed at the end - if (selectionStart !== value.length || selectionEnd !== value.length) return + if (event.altKey || event.ctrlKey) { + // Allow modifier to force nav + } else { + // Shortcut: If at end of input, go to next match AND focus results + const { selectionStart, selectionEnd, value } = searchInput.value! + if (selectionStart !== value.length || selectionEnd !== value.length) return + + // Use the target index (0) if we were at -1 + if (selectedIndex.value === -1) selectedIndex.value = 0 + resultsEl.value?.focus() + } } event.preventDefault() - nextMatch(selectedIndex.value) + nextMatch(targetIndex) // Use targetIndex as we might have just updated selectedIndex from -1 to 0 or kept valid index } }) @@ -897,6 +917,7 @@ function onMouseMove(e: MouseEvent) { :role="results?.length ? 'listbox' : undefined" :aria-labelledby="results?.length ? 'localsearch-label' : undefined" class="results" + tabindex="-1" @mousemove="onMouseMove" >