diff --git a/src/client/theme-default/components/VPLocalSearchBox.vue b/src/client/theme-default/components/VPLocalSearchBox.vue index 649517b3..5458615f 100644 --- a/src/client/theme-default/components/VPLocalSearchBox.vue +++ b/src/client/theme-default/components/VPLocalSearchBox.vue @@ -36,7 +36,8 @@ const emit = defineEmits<{ (e: 'close'): void }>() -const el = ref() +const el = shallowRef() +const resultsEl = shallowRef() /* Search */ @@ -98,8 +99,6 @@ watchEffect(() => { const results: Ref<(SearchResult & Result)[]> = shallowRef([]) -const contents = shallowRef(new Map>()) - const headingRegex = /.*?.*?<\/a><\/h\1>/gi const enableNoResults = ref(false) @@ -108,6 +107,11 @@ watch(filterText, () => { enableNoResults.value = false }) +const mark = computed(() => { + if (!resultsEl.value) return + return new Mark(resultsEl.value) +}) + debouncedWatch( () => [searchIndex.value, filterText.value, showDetailedList.value] as const, async ([index, filterTextValue, showDetailedListValue], old, onCleanup) => { @@ -155,43 +159,38 @@ debouncedWatch( } if (canceled) return } - results.value = results.value.map((r) => { - let title = r.title - let titles = r.titles - let text = '' - // Highlight in text + const terms = new Set() + + results.value = results.value.map((r) => { const [id, anchor] = r.id.split('#') const map = c.get(id) - if (map) { - text = map.get(anchor) ?? '' - } - + const text = map?.get(anchor) ?? '' for (const term in r.match) { - const match = r.match[term] - const reg = new RegExp(term, 'gi') - if (match.includes('title')) { - title = title.replace(reg, `$&`) - } - if (match.includes('titles')) { - titles = titles - .map((t) => t?.replace(reg, `$&`)) - .filter(Boolean) - } + terms.add(term) } - - return { ...r, title, titles, text } + return { ...r, text } }) - contents.value = c await nextTick() + if (canceled) return + + await new Promise((r) => { + mark.value?.unmark({ + done: () => { + mark.value?.markRegExp(formMarkRegex(terms), { done: r }) + } + }) + }) + const excerpts = el.value?.querySelectorAll('.result .excerpt') ?? [] - let i = 0 for (const excerpt of excerpts) { - new Mark(excerpt as HTMLElement).mark(Object.keys(results.value[i].match)) - excerpt.querySelector('mark')?.scrollIntoView({ block: 'center' }) - i += 1 + excerpt + .querySelector('mark[data-markjs="true"]') + ?.scrollIntoView({ block: 'center' }) } + // FIXME: without this whole page scrolls to the bottom + el.value?.querySelector('.result')?.scrollIntoView({ block: 'start' }) }, { debounce: 200, immediate: true } ) @@ -312,6 +311,20 @@ useEventListener('popstate', (event) => { event.preventDefault() emit('close') }) + +function formMarkRegex(terms: Set) { + return new RegExp( + [...terms] + .sort((a, b) => b.length - a.length) + .map((term) => { + return `(${term + .replace(/[|\\{}()[\]^$+*?.]/g, '\\$&') + .replace(/-/g, '\\x2d')})` + }) + .join('|'), + 'gi' + ) +}