From f5d5b11f787abb87225284fb059e3d89e3bcf768 Mon Sep 17 00:00:00 2001 From: "Mr.Hope" Date: Mon, 27 Feb 2023 19:08:32 +0800 Subject: [PATCH] feat(plugin-docsearch): load docsearch asynchronously (close #1247) (#1254) Co-authored-by: meteorlxy --- .../src/client/components/Docsearch.ts | 125 ++++++++++-------- .../src/client/composables/index.ts | 1 + .../composables/useDocsearchHotkeyListener.ts | 16 +++ .../src/client/utils/getFacetFilters.ts | 18 +++ .../client/utils/getSearchButtonTemplate.ts | 14 ++ .../src/client/utils/index.ts | 4 + .../src/client/utils/pollToOpenDocsearch.ts | 15 +++ .../src/client/utils/preconnectToAlgolia.ts | 16 +++ 8 files changed, 155 insertions(+), 54 deletions(-) create mode 100644 ecosystem/plugin-docsearch/src/client/composables/useDocsearchHotkeyListener.ts create mode 100644 ecosystem/plugin-docsearch/src/client/utils/getFacetFilters.ts create mode 100644 ecosystem/plugin-docsearch/src/client/utils/getSearchButtonTemplate.ts create mode 100644 ecosystem/plugin-docsearch/src/client/utils/index.ts create mode 100644 ecosystem/plugin-docsearch/src/client/utils/pollToOpenDocsearch.ts create mode 100644 ecosystem/plugin-docsearch/src/client/utils/preconnectToAlgolia.ts diff --git a/ecosystem/plugin-docsearch/src/client/components/Docsearch.ts b/ecosystem/plugin-docsearch/src/client/components/Docsearch.ts index 01c557b070..3a2ff647df 100644 --- a/ecosystem/plugin-docsearch/src/client/components/Docsearch.ts +++ b/ecosystem/plugin-docsearch/src/client/components/Docsearch.ts @@ -1,14 +1,29 @@ -import { default as docsearch } from '@docsearch/js' import { usePageLang, useRouteLocale } from '@vuepress/client' -import { isArray } from '@vuepress/shared' -import { computed, defineComponent, h, onMounted, watch } from 'vue' -import type { PropType } from 'vue' +import { + computed, + defineComponent, + h, + onMounted, + type PropType, + ref, + watch, +} from 'vue' import type { DocsearchOptions } from '../../shared/index.js' -import { useDocsearchShim } from '../composables/index.js' +import { + useDocsearchHotkeyListener, + useDocsearchShim, +} from '../composables/index.js' +import { + getFacetFilters, + getSearchButtonTemplate, + pollToOpenDocsearch, + preconnectToAlgolia, +} from '../utils/index.js' declare const __DOCSEARCH_INJECT_STYLES__: boolean declare const __DOCSEARCH_OPTIONS__: DocsearchOptions -const options = __DOCSEARCH_OPTIONS__ + +const optionsDefault = __DOCSEARCH_OPTIONS__ if (__DOCSEARCH_INJECT_STYLES__) { import('@docsearch/css') @@ -27,77 +42,79 @@ export const Docsearch = defineComponent({ options: { type: Object as PropType, required: false, - default: () => options, + default: () => optionsDefault, }, }, setup(props) { - const routeLocale = useRouteLocale() - const lang = usePageLang() const docsearchShim = useDocsearchShim() + const lang = usePageLang() + const routeLocale = useRouteLocale() + + const hasInitialized = ref(false) + const hasTriggered = ref(false) // resolve docsearch options for current locale - const optionsLocale = computed(() => ({ + const options = computed(() => ({ ...props.options, ...props.options.locales?.[routeLocale.value], })) - const facetFilters: string[] = [] - - const initialize = (): void => { - const rawFacetFilters = - optionsLocale.value.searchParameters?.facetFilters ?? [] - facetFilters.splice( - 0, - facetFilters.length, - `lang:${lang.value}`, - ...(isArray(rawFacetFilters) ? rawFacetFilters : [rawFacetFilters]) - ) + /** + * Import docsearch js and initialize + */ + const initialize = async (): Promise => { + const { default: docsearch } = await import('@docsearch/js') // @ts-expect-error: https://github.com/microsoft/TypeScript/issues/50690 docsearch({ ...docsearchShim, - ...optionsLocale.value, + ...options.value, container: `#${props.containerId}`, searchParameters: { - ...optionsLocale.value.searchParameters, - facetFilters, + ...options.value.searchParameters, + facetFilters: getFacetFilters( + options.value.searchParameters?.facetFilters, + lang.value + ), }, }) + // mark as initialized + hasInitialized.value = true } - onMounted(() => { + /** + * Trigger docsearch initialization and open it + */ + const trigger = (): void => { + if (hasTriggered.value || hasInitialized.value) return + // mark as triggered + hasTriggered.value = true + // initialize and open initialize() + pollToOpenDocsearch() + // re-initialize when route locale changes + watch(routeLocale, initialize) + } - // re-initialize if the options is changed - watch( - [routeLocale, optionsLocale], - ( - [curRouteLocale, curPropsLocale], - [prevRouteLocale, prevPropsLocale] - ) => { - if (curRouteLocale === prevRouteLocale) return - if ( - JSON.stringify(curPropsLocale) !== JSON.stringify(prevPropsLocale) - ) { - initialize() - } - } - ) + // trigger when hotkey is pressed + useDocsearchHotkeyListener(trigger) - // modify the facetFilters in place to avoid re-initializing docsearch - // when page lang is changed - watch(lang, (curLang, prevLang) => { - if (curLang !== prevLang) { - const prevIndex = facetFilters.findIndex( - (item) => item === `lang:${prevLang}` - ) - if (prevIndex > -1) { - facetFilters.splice(prevIndex, 1, `lang:${curLang}`) - } - } - }) - }) + // preconnect to algolia + onMounted(() => preconnectToAlgolia(options.value.appId)) - return () => h('div', { id: props.containerId }) + return () => [ + h('div', { + id: props.containerId, + style: { display: hasInitialized.value ? 'block' : 'none' }, + }), + hasInitialized.value + ? null + : h('div', { + onClick: trigger, + innerHTML: getSearchButtonTemplate( + options.value.translations?.button + ), + }), + ] }, }) diff --git a/ecosystem/plugin-docsearch/src/client/composables/index.ts b/ecosystem/plugin-docsearch/src/client/composables/index.ts index 10d6f2dc69..553e67822e 100644 --- a/ecosystem/plugin-docsearch/src/client/composables/index.ts +++ b/ecosystem/plugin-docsearch/src/client/composables/index.ts @@ -1 +1,2 @@ +export * from './useDocsearchHotkeyListener.js' export * from './useDocsearchShim.js' diff --git a/ecosystem/plugin-docsearch/src/client/composables/useDocsearchHotkeyListener.ts b/ecosystem/plugin-docsearch/src/client/composables/useDocsearchHotkeyListener.ts new file mode 100644 index 0000000000..dcc1cfad52 --- /dev/null +++ b/ecosystem/plugin-docsearch/src/client/composables/useDocsearchHotkeyListener.ts @@ -0,0 +1,16 @@ +import { onMounted, onUnmounted } from 'vue' + +/** + * Add hotkey listener, remove it after triggered + */ +export const useDocsearchHotkeyListener = (callback: () => void): void => { + const hotkeyListener = (event: KeyboardEvent): void => { + if (event.key === 'k' && (event.ctrlKey || event.metaKey)) { + event.preventDefault() + window.removeEventListener('keydown', hotkeyListener) + callback() + } + } + onMounted(() => window.addEventListener('keydown', hotkeyListener)) + onUnmounted(() => window.removeEventListener('keydown', hotkeyListener)) +} diff --git a/ecosystem/plugin-docsearch/src/client/utils/getFacetFilters.ts b/ecosystem/plugin-docsearch/src/client/utils/getFacetFilters.ts new file mode 100644 index 0000000000..b4cab175cd --- /dev/null +++ b/ecosystem/plugin-docsearch/src/client/utils/getFacetFilters.ts @@ -0,0 +1,18 @@ +import { isArray } from '@vuepress/shared' +import type { DocsearchOptions } from '../../shared/index.js' + +type FacetFilters = + Required['searchParameters']['facetFilters'] + +/** + * Get facet filters for current lang + */ +export const getFacetFilters = ( + rawFacetFilters: FacetFilters = [], + lang: string +): FacetFilters => [ + `lang:${lang}`, + ...((isArray(rawFacetFilters) + ? rawFacetFilters + : [rawFacetFilters]) as string[]), +] diff --git a/ecosystem/plugin-docsearch/src/client/utils/getSearchButtonTemplate.ts b/ecosystem/plugin-docsearch/src/client/utils/getSearchButtonTemplate.ts new file mode 100644 index 0000000000..b23fe2805e --- /dev/null +++ b/ecosystem/plugin-docsearch/src/client/utils/getSearchButtonTemplate.ts @@ -0,0 +1,14 @@ +import type { DocSearchTranslations } from '@docsearch/react' + +/** + * Get the search button template + * + * Use the same content as in @docsearch/js + * + * TODO: the meta key text should also be dynamic + */ +export const getSearchButtonTemplate = ({ + buttonText = 'Search', + buttonAriaLabel = buttonText, +}: DocSearchTranslations['button'] = {}): string => + `` diff --git a/ecosystem/plugin-docsearch/src/client/utils/index.ts b/ecosystem/plugin-docsearch/src/client/utils/index.ts new file mode 100644 index 0000000000..5f03cf030f --- /dev/null +++ b/ecosystem/plugin-docsearch/src/client/utils/index.ts @@ -0,0 +1,4 @@ +export * from './getFacetFilters.js' +export * from './getSearchButtonTemplate.js' +export * from './pollToOpenDocsearch.js' +export * from './preconnectToAlgolia.js' diff --git a/ecosystem/plugin-docsearch/src/client/utils/pollToOpenDocsearch.ts b/ecosystem/plugin-docsearch/src/client/utils/pollToOpenDocsearch.ts new file mode 100644 index 0000000000..1805aecb09 --- /dev/null +++ b/ecosystem/plugin-docsearch/src/client/utils/pollToOpenDocsearch.ts @@ -0,0 +1,15 @@ +const POLL_INTERVAL = 16 + +/** + * Programmatically open the docsearch modal + */ +export const pollToOpenDocsearch = (): void => { + if (document.querySelector('.DocSearch-Modal')) return + const e = new Event('keydown') as { + -readonly [P in keyof KeyboardEvent]: KeyboardEvent[P] + } + e.key = 'k' + e.metaKey = true + window.dispatchEvent(e) + setTimeout(pollToOpenDocsearch, POLL_INTERVAL) +} diff --git a/ecosystem/plugin-docsearch/src/client/utils/preconnectToAlgolia.ts b/ecosystem/plugin-docsearch/src/client/utils/preconnectToAlgolia.ts new file mode 100644 index 0000000000..6dc934524e --- /dev/null +++ b/ecosystem/plugin-docsearch/src/client/utils/preconnectToAlgolia.ts @@ -0,0 +1,16 @@ +/** + * Preconnect to Algolia's API + */ +export const preconnectToAlgolia = (appId: string): void => { + const id = 'algolia-preconnect' + const rIC = window.requestIdleCallback || setTimeout + rIC(() => { + if (document.head.querySelector(`#${id}`)) return + const preconnect = document.createElement('link') + preconnect.id = id + preconnect.rel = 'preconnect' + preconnect.href = `https://${appId}-dsn.algolia.net` + preconnect.crossOrigin = '' + document.head.appendChild(preconnect) + }) +}