Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(plugin-docsearch): make docsearch async and only loads at click, close #1247 #1254

Merged
merged 7 commits into from Feb 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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__) {
Mister-Hope marked this conversation as resolved.
Show resolved Hide resolved
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)
})
}