Files
tabler/docs/components/DocsSearch.astro
T

98 lines
3.5 KiB
Plaintext

---
// Docs search: a native Tabler input that opens the DocSearch modal.
// Lives in the sidebar, above the menu.
import Icon from '@ui/Icon.astro'
---
<!-- BEGIN DOCS SEARCH -->
<div class="input-icon docs-search">
<span class="input-icon-addon">
<Icon name="search" />
</span>
<input type="search" id="docs-search-input" class="form-control cursor-pointer" placeholder="Search…" aria-label="Search documentation" autocomplete="off" readonly />
<span class="input-icon-addon">
<span id="docs-search-kbd" class="fs-4">⌘K</span>
</span>
</div>
<div id="docsearch" class="d-none"></div>
<!-- END DOCS SEARCH -->
<script>
// Lazy DocSearch: the native input above triggers a dynamic import of
// @docsearch/js, which Vite splits into its own hashed chunk — nothing is
// downloaded until the first interaction. DocSearch mounts into the hidden
// #docsearch container; opening the modal means clicking its hidden button.
import { DOCSEARCH_APP_ID, DOCSEARCH_INDEX_NAME, DOCSEARCH_API_KEY } from 'astro:env/client'
const input = document.getElementById('docs-search-input')
const mount = document.getElementById('docsearch')
// Local consts so TypeScript narrows `string | undefined` inside the closures below.
const appId = DOCSEARCH_APP_ID
const indexName = DOCSEARCH_INDEX_NAME
const apiKey = DOCSEARCH_API_KEY
if (input && !(appId && indexName && apiKey)) {
// No Algolia env config (see .env.example) — hide the dead search input.
input.closest<HTMLElement>('.input-icon')?.classList.add('d-none')
console.warn('Docs search disabled: DOCSEARCH_* env variables are not set.')
}
if (input && mount && appId && indexName && apiKey) {
const isMac = /Mac|iPhone|iPad|iPod/.test(navigator.userAgent)
const kbd = document.getElementById('docs-search-kbd')
if (kbd && !isMac) kbd.textContent = 'Ctrl K'
let state: 'idle' | 'loading' | 'ready' = 'idle'
let openWhenReady = false
const openModal = () => mount.querySelector<HTMLElement>('.DocSearch-Button')?.click()
const load = async (open: boolean) => {
openWhenReady = openWhenReady || open
if (state === 'ready') {
if (open) openModal()
return
}
if (state === 'loading') return
state = 'loading'
try {
// The package root is the Ask AI build in v5; /docsearch is plain keyword search.
const { default: docsearch } = await import('@docsearch/js/docsearch')
docsearch({
container: '#docsearch',
appId,
// DocSearch 5 takes a list of indices instead of a single indexName.
indices: [indexName],
apiKey,
})
state = 'ready'
// The bundle renders its (hidden) button and binds its own shortcuts.
if (openWhenReady) openModal()
} catch {
state = 'idle'
}
}
input.addEventListener('click', () => {
load(true)
input.blur()
})
// Keyboard users: prefetch on focus, open on Enter or when they start typing.
input.addEventListener('focus', () => load(false))
input.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key.length === 1) {
event.preventDefault()
load(true)
input.blur()
}
})
input.addEventListener('pointerenter', () => load(false))
document.addEventListener('keydown', (event) => {
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k' && state !== 'ready') {
event.preventDefault()
load(true)
}
})
}
</script>