Skip to content

Commit

Permalink
feat(plugin-docsearch): load docsearch asynchronously (close #1247) (#…
Browse files Browse the repository at this point in the history
…1254)

Co-authored-by: meteorlxy <meteor.lxy@foxmail.com>
  • Loading branch information
Mister-Hope and meteorlxy committed Feb 27, 2023
1 parent 346c6e7 commit f5d5b11
Show file tree
Hide file tree
Showing 8 changed files with 155 additions and 54 deletions.
125 changes: 71 additions & 54 deletions 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')
Expand All @@ -27,77 +42,79 @@ export const Docsearch = defineComponent({
options: {
type: Object as PropType<DocsearchOptions>,
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<void> => {
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
),
}),
]
},
})
1 change: 1 addition & 0 deletions ecosystem/plugin-docsearch/src/client/composables/index.ts
@@ -1 +1,2 @@
export * from './useDocsearchHotkeyListener.js'
export * from './useDocsearchShim.js'
@@ -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))
}
18 changes: 18 additions & 0 deletions 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<DocsearchOptions>['searchParameters']['facetFilters']

/**
* Get facet filters for current lang
*/
export const getFacetFilters = (
rawFacetFilters: FacetFilters = [],
lang: string
): FacetFilters => [
`lang:${lang}`,
...((isArray(rawFacetFilters)
? rawFacetFilters
: [rawFacetFilters]) as string[]),
]
@@ -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 =>
`<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>`
4 changes: 4 additions & 0 deletions 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'
15 changes: 15 additions & 0 deletions 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)
}
16 changes: 16 additions & 0 deletions 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)
})
}

0 comments on commit f5d5b11

Please sign in to comment.