Warp/node_modules/vitepress/dist/client/theme-default/components/VPAlgoliaSearchBox.vue

248 lines
7.3 KiB
Vue
Raw Normal View History

2024-01-05 12:14:38 +00:00
<script setup lang="ts">
import type { DocSearchInstance, DocSearchProps } from '@docsearch/js'
import type { SidepanelInstance, SidepanelProps } from '@docsearch/sidepanel-js'
import { inBrowser, useRouter } from 'vitepress'
2024-01-05 12:14:38 +00:00
import type { DefaultTheme } from 'vitepress/theme'
import { nextTick, onUnmounted, watch } from 'vue'
import type { DocSearchAskAi } from '../../../../types/docsearch'
2024-01-05 12:14:38 +00:00
import { useData } from '../composables/data'
import { resolveMode, validateCredentials } from '../support/docsearch'
import '../styles/docsearch.css'
2024-01-05 12:14:38 +00:00
const props = defineProps<{
algoliaOptions: DefaultTheme.AlgoliaSearchOptions
openRequest?: {
target: 'search' | 'askAi' | 'toggleAskAi'
nonce: number
} | null
2024-01-05 12:14:38 +00:00
}>()
const router = useRouter()
const { site } = useData()
2024-01-05 12:14:38 +00:00
let cleanup = () => {}
let docsearchInstance: DocSearchInstance | undefined
let sidepanelInstance: SidepanelInstance | undefined
let openOnReady: 'search' | 'askAi' | null = null
let initializeCount = 0
let docsearchLoader: Promise<typeof import('@docsearch/js')> | undefined
let sidepanelLoader: Promise<typeof import('@docsearch/sidepanel-js')> | undefined
let lastFocusedElement: HTMLElement | null = null
let skipEventDocsearch = false
let skipEventSidepanel = false
2024-01-05 12:14:38 +00:00
watch(() => props.algoliaOptions, update, { immediate: true })
onUnmounted(cleanup)
2024-01-05 12:14:38 +00:00
watch(
() => props.openRequest?.nonce,
() => {
const req = props.openRequest
if (!req) return
if (req.target === 'search') {
if (docsearchInstance?.isReady) {
onBeforeOpen('docsearch', () => docsearchInstance?.open())
} else {
openOnReady = 'search'
}
} else if (req.target === 'toggleAskAi') {
if (sidepanelInstance?.isOpen) {
sidepanelInstance.close()
} else {
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
}
} else {
// askAi - open sidepanel or fallback to docsearch modal
if (sidepanelInstance?.isReady) {
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
} else if (sidepanelInstance) {
openOnReady = 'askAi'
} else if (docsearchInstance?.isReady) {
onBeforeOpen('docsearch', () => docsearchInstance?.openAskAi())
} else {
openOnReady = 'askAi'
}
2024-01-05 12:14:38 +00:00
}
},
{ immediate: true }
)
async function update(options: DefaultTheme.AlgoliaSearchOptions) {
if (!inBrowser) return
await nextTick()
const askAi = options.askAi as DocSearchAskAi | undefined
const { valid, ...credentials } = validateCredentials({
appId: options.appId ?? askAi?.appId,
apiKey: options.apiKey ?? askAi?.apiKey,
indexName: options.indexName ?? askAi?.indexName
2024-01-05 12:14:38 +00:00
})
if (!valid) {
console.warn('[vitepress] Algolia search cannot be initialized: missing appId/apiKey/indexName.')
return
}
await initialize({ ...options, ...credentials })
2024-01-05 12:14:38 +00:00
}
async function initialize(userOptions: DefaultTheme.AlgoliaSearchOptions) {
const currentInitialize = ++initializeCount
2024-01-05 12:14:38 +00:00
// Always tear down previous instances first (e.g. on locale changes)
cleanup()
const { useSidePanel } = resolveMode(userOptions)
const askAi = userOptions.askAi as DocSearchAskAi | undefined
const { default: docsearch } = await loadDocsearch()
if (currentInitialize !== initializeCount) return
if (useSidePanel && askAi?.sidePanel) {
const { default: sidepanel } = await loadSidepanel()
if (currentInitialize !== initializeCount) return
sidepanelInstance = sidepanel({
...(askAi.sidePanel === true ? {} : askAi.sidePanel),
container: '#vp-docsearch-sidepanel',
indexName: askAi.indexName ?? userOptions.indexName,
appId: askAi.appId ?? userOptions.appId,
apiKey: askAi.apiKey ?? userOptions.apiKey,
assistantId: askAi.assistantId,
onOpen: focusInput,
onClose: onClose.bind(null, 'sidepanel'),
onReady: () => {
if (openOnReady === 'askAi') {
openOnReady = null
onBeforeOpen('sidepanel', () => sidepanelInstance?.open())
2024-01-05 12:14:38 +00:00
}
},
keyboardShortcuts: {
'Ctrl/Cmd+I': false
2024-01-05 12:14:38 +00:00
}
} as SidepanelProps)
}
2024-01-05 12:14:38 +00:00
const options = {
...userOptions,
container: '#vp-docsearch',
navigator: {
navigate(item) {
router.go(item.itemUrl)
}
2024-01-05 12:14:38 +00:00
},
transformItems: (items) => items.map((item) => ({ ...item, url: getRelativePath(item.url) })),
// When sidepanel is enabled, intercept Ask AI events to open it instead (hybrid mode)
...(useSidePanel && sidepanelInstance && {
interceptAskAiEvent: (initialMessage) => {
onBeforeOpen('sidepanel', () => sidepanelInstance?.open(initialMessage))
return true
}
}),
onOpen: focusInput,
onClose: onClose.bind(null, 'docsearch'),
onReady: () => {
if (openOnReady === 'search') {
openOnReady = null
onBeforeOpen('docsearch', () => docsearchInstance?.open())
} else if (openOnReady === 'askAi' && !sidepanelInstance) {
// No sidepanel configured, use docsearch modal for askAi
openOnReady = null
onBeforeOpen('docsearch', () => docsearchInstance?.openAskAi())
}
},
keyboardShortcuts: {
'/': false,
'Ctrl/Cmd+K': false
}
} as DocSearchProps
docsearchInstance = docsearch(options)
cleanup = () => {
docsearchInstance?.destroy()
sidepanelInstance?.destroy()
docsearchInstance = undefined
sidepanelInstance = undefined
openOnReady = null
lastFocusedElement = null
}
}
function focusInput() {
requestAnimationFrame(() => {
const input =
document.querySelector<HTMLInputElement>('#docsearch-input') ||
document.querySelector<HTMLInputElement>('#docsearch-sidepanel textarea')
input?.focus()
})
}
2024-01-05 12:14:38 +00:00
function onBeforeOpen(target: 'docsearch' | 'sidepanel', cb: () => void) {
if (target === 'docsearch') {
if (sidepanelInstance?.isOpen) {
skipEventSidepanel = true
sidepanelInstance.close()
} else if (!docsearchInstance?.isOpen) {
if (document.activeElement instanceof HTMLElement) {
lastFocusedElement = document.activeElement
}
}
} else if (target === 'sidepanel') {
if (docsearchInstance?.isOpen) {
skipEventDocsearch = true
docsearchInstance.close()
} else if (!sidepanelInstance?.isOpen) {
if (document.activeElement instanceof HTMLElement) {
lastFocusedElement = document.activeElement
2024-01-05 12:14:38 +00:00
}
}
}
setTimeout(cb, 0)
}
function onClose(target: 'docsearch' | 'sidepanel') {
if (target === 'docsearch') {
if (skipEventDocsearch) {
skipEventDocsearch = false
return
}
} else if (target === 'sidepanel') {
if (skipEventSidepanel) {
skipEventSidepanel = false
return
}
}
if (lastFocusedElement) {
lastFocusedElement.focus()
lastFocusedElement = null
}
}
function loadDocsearch() {
if (!docsearchLoader) {
docsearchLoader = import('@docsearch/js')
}
return docsearchLoader
}
2024-01-05 12:14:38 +00:00
function loadSidepanel() {
if (!sidepanelLoader) {
sidepanelLoader = import('@docsearch/sidepanel-js')
}
return sidepanelLoader
2024-01-05 12:14:38 +00:00
}
function getRelativePath(url: string) {
const { pathname, hash } = new URL(url, location.origin)
return pathname.replace(/\.html$/, site.value.cleanUrls ? '' : '.html') + hash
}
</script>
<template>
<div id="vp-docsearch" />
<div id="vp-docsearch-sidepanel" />
2024-01-05 12:14:38 +00:00
</template>