Skip to content

Commit f5d5b11

Browse files
feat(plugin-docsearch): load docsearch asynchronously (close #1247) (#1254)
Co-authored-by: meteorlxy <meteor.lxy@foxmail.com>
1 parent 346c6e7 commit f5d5b11

File tree

8 files changed

+155
-54
lines changed

8 files changed

+155
-54
lines changed
Lines changed: 71 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,29 @@
1-
import { default as docsearch } from '@docsearch/js'
21
import { usePageLang, useRouteLocale } from '@vuepress/client'
3-
import { isArray } from '@vuepress/shared'
4-
import { computed, defineComponent, h, onMounted, watch } from 'vue'
5-
import type { PropType } from 'vue'
2+
import {
3+
computed,
4+
defineComponent,
5+
h,
6+
onMounted,
7+
type PropType,
8+
ref,
9+
watch,
10+
} from 'vue'
611
import type { DocsearchOptions } from '../../shared/index.js'
7-
import { useDocsearchShim } from '../composables/index.js'
12+
import {
13+
useDocsearchHotkeyListener,
14+
useDocsearchShim,
15+
} from '../composables/index.js'
16+
import {
17+
getFacetFilters,
18+
getSearchButtonTemplate,
19+
pollToOpenDocsearch,
20+
preconnectToAlgolia,
21+
} from '../utils/index.js'
822

923
declare const __DOCSEARCH_INJECT_STYLES__: boolean
1024
declare const __DOCSEARCH_OPTIONS__: DocsearchOptions
11-
const options = __DOCSEARCH_OPTIONS__
25+
26+
const optionsDefault = __DOCSEARCH_OPTIONS__
1227

1328
if (__DOCSEARCH_INJECT_STYLES__) {
1429
import('@docsearch/css')
@@ -27,77 +42,79 @@ export const Docsearch = defineComponent({
2742
options: {
2843
type: Object as PropType<DocsearchOptions>,
2944
required: false,
30-
default: () => options,
45+
default: () => optionsDefault,
3146
},
3247
},
3348

3449
setup(props) {
35-
const routeLocale = useRouteLocale()
36-
const lang = usePageLang()
3750
const docsearchShim = useDocsearchShim()
51+
const lang = usePageLang()
52+
const routeLocale = useRouteLocale()
53+
54+
const hasInitialized = ref(false)
55+
const hasTriggered = ref(false)
3856

3957
// resolve docsearch options for current locale
40-
const optionsLocale = computed(() => ({
58+
const options = computed(() => ({
4159
...props.options,
4260
...props.options.locales?.[routeLocale.value],
4361
}))
4462

45-
const facetFilters: string[] = []
46-
47-
const initialize = (): void => {
48-
const rawFacetFilters =
49-
optionsLocale.value.searchParameters?.facetFilters ?? []
50-
facetFilters.splice(
51-
0,
52-
facetFilters.length,
53-
`lang:${lang.value}`,
54-
...(isArray(rawFacetFilters) ? rawFacetFilters : [rawFacetFilters])
55-
)
63+
/**
64+
* Import docsearch js and initialize
65+
*/
66+
const initialize = async (): Promise<void> => {
67+
const { default: docsearch } = await import('@docsearch/js')
5668
// @ts-expect-error: https://github.com/microsoft/TypeScript/issues/50690
5769
docsearch({
5870
...docsearchShim,
59-
...optionsLocale.value,
71+
...options.value,
6072
container: `#${props.containerId}`,
6173
searchParameters: {
62-
...optionsLocale.value.searchParameters,
63-
facetFilters,
74+
...options.value.searchParameters,
75+
facetFilters: getFacetFilters(
76+
options.value.searchParameters?.facetFilters,
77+
lang.value
78+
),
6479
},
6580
})
81+
// mark as initialized
82+
hasInitialized.value = true
6683
}
6784

68-
onMounted(() => {
85+
/**
86+
* Trigger docsearch initialization and open it
87+
*/
88+
const trigger = (): void => {
89+
if (hasTriggered.value || hasInitialized.value) return
90+
// mark as triggered
91+
hasTriggered.value = true
92+
// initialize and open
6993
initialize()
94+
pollToOpenDocsearch()
95+
// re-initialize when route locale changes
96+
watch(routeLocale, initialize)
97+
}
7098

71-
// re-initialize if the options is changed
72-
watch(
73-
[routeLocale, optionsLocale],
74-
(
75-
[curRouteLocale, curPropsLocale],
76-
[prevRouteLocale, prevPropsLocale]
77-
) => {
78-
if (curRouteLocale === prevRouteLocale) return
79-
if (
80-
JSON.stringify(curPropsLocale) !== JSON.stringify(prevPropsLocale)
81-
) {
82-
initialize()
83-
}
84-
}
85-
)
99+
// trigger when hotkey is pressed
100+
useDocsearchHotkeyListener(trigger)
86101

87-
// modify the facetFilters in place to avoid re-initializing docsearch
88-
// when page lang is changed
89-
watch(lang, (curLang, prevLang) => {
90-
if (curLang !== prevLang) {
91-
const prevIndex = facetFilters.findIndex(
92-
(item) => item === `lang:${prevLang}`
93-
)
94-
if (prevIndex > -1) {
95-
facetFilters.splice(prevIndex, 1, `lang:${curLang}`)
96-
}
97-
}
98-
})
99-
})
102+
// preconnect to algolia
103+
onMounted(() => preconnectToAlgolia(options.value.appId))
100104

101-
return () => h('div', { id: props.containerId })
105+
return () => [
106+
h('div', {
107+
id: props.containerId,
108+
style: { display: hasInitialized.value ? 'block' : 'none' },
109+
}),
110+
hasInitialized.value
111+
? null
112+
: h('div', {
113+
onClick: trigger,
114+
innerHTML: getSearchButtonTemplate(
115+
options.value.translations?.button
116+
),
117+
}),
118+
]
102119
},
103120
})
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
1+
export * from './useDocsearchHotkeyListener.js'
12
export * from './useDocsearchShim.js'
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { onMounted, onUnmounted } from 'vue'
2+
3+
/**
4+
* Add hotkey listener, remove it after triggered
5+
*/
6+
export const useDocsearchHotkeyListener = (callback: () => void): void => {
7+
const hotkeyListener = (event: KeyboardEvent): void => {
8+
if (event.key === 'k' && (event.ctrlKey || event.metaKey)) {
9+
event.preventDefault()
10+
window.removeEventListener('keydown', hotkeyListener)
11+
callback()
12+
}
13+
}
14+
onMounted(() => window.addEventListener('keydown', hotkeyListener))
15+
onUnmounted(() => window.removeEventListener('keydown', hotkeyListener))
16+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { isArray } from '@vuepress/shared'
2+
import type { DocsearchOptions } from '../../shared/index.js'
3+
4+
type FacetFilters =
5+
Required<DocsearchOptions>['searchParameters']['facetFilters']
6+
7+
/**
8+
* Get facet filters for current lang
9+
*/
10+
export const getFacetFilters = (
11+
rawFacetFilters: FacetFilters = [],
12+
lang: string
13+
): FacetFilters => [
14+
`lang:${lang}`,
15+
...((isArray(rawFacetFilters)
16+
? rawFacetFilters
17+
: [rawFacetFilters]) as string[]),
18+
]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import type { DocSearchTranslations } from '@docsearch/react'
2+
3+
/**
4+
* Get the search button template
5+
*
6+
* Use the same content as in @docsearch/js
7+
*
8+
* TODO: the meta key text should also be dynamic
9+
*/
10+
export const getSearchButtonTemplate = ({
11+
buttonText = 'Search',
12+
buttonAriaLabel = buttonText,
13+
}: DocSearchTranslations['button'] = {}): string =>
14+
`<button type="button" class="DocSearch DocSearch-Button" aria-label="${buttonAriaLabel}"><span class="DocSearch-Button-Container"><svg width="20" height="20" class="DocSearch-Search-Icon" viewBox="0 0 20 20"><path d="M14.386 14.386l4.0877 4.0877-4.0877-4.0877c-2.9418 2.9419-7.7115 2.9419-10.6533 0-2.9419-2.9418-2.9419-7.7115 0-10.6533 2.9418-2.9419 7.7115-2.9419 10.6533 0 2.9419 2.9418 2.9419 7.7115 0 10.6533z" stroke="currentColor" fill="none" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round"></path></svg><span class="DocSearch-Button-Placeholder">${buttonText}</span></span><span class="DocSearch-Button-Keys"><kbd class="DocSearch-Button-Key"><svg width="15" height="15" class="DocSearch-Control-Key-Icon"><path d="M4.505 4.496h2M5.505 5.496v5M8.216 4.496l.055 5.993M10 7.5c.333.333.5.667.5 1v2M12.326 4.5v5.996M8.384 4.496c1.674 0 2.116 0 2.116 1.5s-.442 1.5-2.116 1.5M3.205 9.303c-.09.448-.277 1.21-1.241 1.203C1 10.5.5 9.513.5 8V7c0-1.57.5-2.5 1.464-2.494.964.006 1.134.598 1.24 1.342M12.553 10.5h1.953" stroke-width="1.2" stroke="currentColor" fill="none" stroke-linecap="square"></path></svg></kbd><kbd class="DocSearch-Button-Key">K</kbd></span></button>`
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export * from './getFacetFilters.js'
2+
export * from './getSearchButtonTemplate.js'
3+
export * from './pollToOpenDocsearch.js'
4+
export * from './preconnectToAlgolia.js'
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
const POLL_INTERVAL = 16
2+
3+
/**
4+
* Programmatically open the docsearch modal
5+
*/
6+
export const pollToOpenDocsearch = (): void => {
7+
if (document.querySelector('.DocSearch-Modal')) return
8+
const e = new Event('keydown') as {
9+
-readonly [P in keyof KeyboardEvent]: KeyboardEvent[P]
10+
}
11+
e.key = 'k'
12+
e.metaKey = true
13+
window.dispatchEvent(e)
14+
setTimeout(pollToOpenDocsearch, POLL_INTERVAL)
15+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
/**
2+
* Preconnect to Algolia's API
3+
*/
4+
export const preconnectToAlgolia = (appId: string): void => {
5+
const id = 'algolia-preconnect'
6+
const rIC = window.requestIdleCallback || setTimeout
7+
rIC(() => {
8+
if (document.head.querySelector(`#${id}`)) return
9+
const preconnect = document.createElement('link')
10+
preconnect.id = id
11+
preconnect.rel = 'preconnect'
12+
preconnect.href = `https://${appId}-dsn.algolia.net`
13+
preconnect.crossOrigin = ''
14+
document.head.appendChild(preconnect)
15+
})
16+
}

0 commit comments

Comments
 (0)