From 1b6dc9869f8114d9dac640025a65e810039b344f Mon Sep 17 00:00:00 2001 From: meteorlxy Date: Mon, 27 Feb 2023 18:49:19 +0800 Subject: [PATCH] refactor: docsearch --- .../src/client/components/Docsearch.ts | 94 ++++++++++-- .../src/client/composables/index.ts | 2 +- .../src/client/composables/useDocSearch.ts | 145 ------------------ .../composables/useDocsearchHotkeyListener.ts | 16 ++ .../src/client/utils/getFacetFilters.ts | 18 +++ .../client/utils/getSearchButtonTemplate.ts | 14 ++ .../src/client/utils/index.ts | 7 +- .../src/client/utils/options.ts | 5 - .../src/client/utils/pollToOpenDocsearch.ts | 15 ++ ...nnectAlgolia.ts => preconnectToAlgolia.ts} | 11 +- .../src/client/utils/searchButtonTemplate.ts | 5 - 11 files changed, 157 insertions(+), 175 deletions(-) delete mode 100644 ecosystem/plugin-docsearch/src/client/composables/useDocSearch.ts 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 delete mode 100644 ecosystem/plugin-docsearch/src/client/utils/options.ts create mode 100644 ecosystem/plugin-docsearch/src/client/utils/pollToOpenDocsearch.ts rename ecosystem/plugin-docsearch/src/client/utils/{preconnectAlgolia.ts => preconnectToAlgolia.ts} (68%) delete mode 100644 ecosystem/plugin-docsearch/src/client/utils/searchButtonTemplate.ts diff --git a/ecosystem/plugin-docsearch/src/client/components/Docsearch.ts b/ecosystem/plugin-docsearch/src/client/components/Docsearch.ts index 2b1777f539..3a2ff647df 100644 --- a/ecosystem/plugin-docsearch/src/client/components/Docsearch.ts +++ b/ecosystem/plugin-docsearch/src/client/components/Docsearch.ts @@ -1,10 +1,29 @@ -import { defineComponent, h } from 'vue' -import type { PropType } from 'vue' +import { usePageLang, useRouteLocale } from '@vuepress/client' +import { + computed, + defineComponent, + h, + onMounted, + type PropType, + ref, + watch, +} from 'vue' import type { DocsearchOptions } from '../../shared/index.js' -import { useDocsearch } from '../composables/index.js' -import { options, searchButtonTemplate } from '../utils/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 optionsDefault = __DOCSEARCH_OPTIONS__ if (__DOCSEARCH_INJECT_STYLES__) { import('@docsearch/css') @@ -23,23 +42,78 @@ export const Docsearch = defineComponent({ options: { type: Object as PropType, required: false, - default: () => options, + default: () => optionsDefault, }, }, setup(props) { - const { loaded, loadDocsearch } = useDocsearch(props) + const docsearchShim = useDocsearchShim() + const lang = usePageLang() + const routeLocale = useRouteLocale() + + const hasInitialized = ref(false) + const hasTriggered = ref(false) + + // resolve docsearch options for current locale + const options = computed(() => ({ + ...props.options, + ...props.options.locales?.[routeLocale.value], + })) + + /** + * 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, + ...options.value, + container: `#${props.containerId}`, + searchParameters: { + ...options.value.searchParameters, + facetFilters: getFacetFilters( + options.value.searchParameters?.facetFilters, + lang.value + ), + }, + }) + // mark as initialized + hasInitialized.value = true + } + + /** + * 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) + } + + // trigger when hotkey is pressed + useDocsearchHotkeyListener(trigger) + + // preconnect to algolia + onMounted(() => preconnectToAlgolia(options.value.appId)) return () => [ h('div', { id: props.containerId, - style: { display: loaded.value ? 'block' : 'none' }, + style: { display: hasInitialized.value ? 'block' : 'none' }, }), - loaded.value + hasInitialized.value ? null : h('div', { - onClick: () => loadDocsearch(), - innerHTML: searchButtonTemplate, + 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 8e6713b738..553e67822e 100644 --- a/ecosystem/plugin-docsearch/src/client/composables/index.ts +++ b/ecosystem/plugin-docsearch/src/client/composables/index.ts @@ -1,2 +1,2 @@ -export * from './useDocSearch.js' +export * from './useDocsearchHotkeyListener.js' export * from './useDocsearchShim.js' diff --git a/ecosystem/plugin-docsearch/src/client/composables/useDocSearch.ts b/ecosystem/plugin-docsearch/src/client/composables/useDocSearch.ts deleted file mode 100644 index 37c56ed49f..0000000000 --- a/ecosystem/plugin-docsearch/src/client/composables/useDocSearch.ts +++ /dev/null @@ -1,145 +0,0 @@ -import { usePageLang, useRouteLocale } from '@vuepress/client' -import { isArray } from '@vuepress/shared' -import { computed, onMounted, onUnmounted, readonly, ref, watch } from 'vue' -import type { Ref, WatchStopHandle } from 'vue' -import { preconnectAlgolia } from '../utils/index.js' -import { useDocsearchShim } from './useDocsearchShim.js' - -export interface Docsearch { - loaded: Readonly> - loadDocsearch: () => void -} - -export const useDocsearch = (props): Docsearch => { - const routeLocale = useRouteLocale() - const lang = usePageLang() - const docsearchShim = useDocsearchShim() - - // to avoid loading the docsearch js upfront (which is more than 1/3 of the - // payload), we delay initializing it until the user has actually clicked or - // hit the hotkey to invoke it. - const loading = ref(false) - const loaded = ref(false) - const metaKey = ref(`'Meta'`) - - // resolve docsearch options for current locale - const optionsLocale = computed(() => ({ - ...props.options, - ...props.options.locales?.[routeLocale.value], - })) - - const facetFilters: string[] = [] - const stopHandle: WatchStopHandle[] = [] - - const initialize = async (): Promise => { - const { default: docsearch } = await import('@docsearch/js') - - const rawFacetFilters = - optionsLocale.value.searchParameters?.facetFilters ?? [] - facetFilters.splice( - 0, - facetFilters.length, - `lang:${lang.value}`, - ...(isArray(rawFacetFilters) ? rawFacetFilters : [rawFacetFilters]) - ) - // @ts-expect-error: https://github.com/microsoft/TypeScript/issues/50690 - docsearch({ - ...docsearchShim, - ...optionsLocale.value, - container: `#${props.containerId}`, - searchParameters: { - ...optionsLocale.value.searchParameters, - facetFilters, - }, - }) - - loaded.value = true - } - - const poll = (): void => { - // programmatically open the search box after initialize - const e = new Event('keydown') as any - e.key = 'k' - e.metaKey = true - window.dispatchEvent(e) - setTimeout(() => { - if (!document.querySelector('.DocSearch-Modal')) { - poll() - } - }, 16) - } - - const load = (): void => { - if (!loading.value) { - loading.value = true - initialize() - setTimeout(poll, 16) - } - } - - onMounted(() => { - preconnectAlgolia() - - // meta key detect (same logic as in @docsearch/js) - metaKey.value = /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform) - ? `'⌘'` - : `'Ctrl'` - - const handleSearchHotKey = (e: KeyboardEvent): void => { - if (e.key === 'k' && (e.ctrlKey || e.metaKey)) { - e.preventDefault() - load() - remove() - } - } - const remove = (): void => { - window.removeEventListener('keydown', handleSearchHotKey) - } - window.addEventListener('keydown', handleSearchHotKey) - onUnmounted(remove) - }) - - onUnmounted(() => { - stopHandle.forEach((item) => item()) - }) - - const loadDocsearch = (): void => { - load() - - // re-initialize if the options is changed - stopHandle.push( - watch( - [routeLocale, optionsLocale], - ( - [curRouteLocale, curPropsLocale], - [prevRouteLocale, prevPropsLocale] - ) => { - if (curRouteLocale === prevRouteLocale) return - if ( - JSON.stringify(curPropsLocale) !== JSON.stringify(prevPropsLocale) - ) { - initialize() - } - } - ), - - // 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}`) - } - } - }) - ) - } - - return { - loaded: readonly(loaded), - loadDocsearch, - } -} 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 index 9247c337e5..5f03cf030f 100644 --- a/ecosystem/plugin-docsearch/src/client/utils/index.ts +++ b/ecosystem/plugin-docsearch/src/client/utils/index.ts @@ -1,3 +1,4 @@ -export * from './options.js' -export * from './preconnectAlgolia.js' -export * from './searchButtonTemplate.js' +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/options.ts b/ecosystem/plugin-docsearch/src/client/utils/options.ts deleted file mode 100644 index c66977ccad..0000000000 --- a/ecosystem/plugin-docsearch/src/client/utils/options.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { DocsearchOptions } from '../../shared/index.js' - -declare const __DOCSEARCH_OPTIONS__: DocsearchOptions - -export const options = __DOCSEARCH_OPTIONS__ 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/preconnectAlgolia.ts b/ecosystem/plugin-docsearch/src/client/utils/preconnectToAlgolia.ts similarity index 68% rename from ecosystem/plugin-docsearch/src/client/utils/preconnectAlgolia.ts rename to ecosystem/plugin-docsearch/src/client/utils/preconnectToAlgolia.ts index c2acc9fb65..6dc934524e 100644 --- a/ecosystem/plugin-docsearch/src/client/utils/preconnectAlgolia.ts +++ b/ecosystem/plugin-docsearch/src/client/utils/preconnectToAlgolia.ts @@ -1,16 +1,15 @@ -import { options } from './options.js' - -export const preconnectAlgolia = (): void => { +/** + * 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://${options.appId}-dsn.algolia.net` + preconnect.href = `https://${appId}-dsn.algolia.net` preconnect.crossOrigin = '' document.head.appendChild(preconnect) }) diff --git a/ecosystem/plugin-docsearch/src/client/utils/searchButtonTemplate.ts b/ecosystem/plugin-docsearch/src/client/utils/searchButtonTemplate.ts deleted file mode 100644 index ff9d9a9ad6..0000000000 --- a/ecosystem/plugin-docsearch/src/client/utils/searchButtonTemplate.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * search button template (same content as in @docsearch/js) - */ -export const searchButtonTemplate = - ''