(() => { // Append the search dialog to the body const siteSearch = document.createElement('div'); const scrollbarWidth = Math.abs(window.innerWidth - document.documentElement.clientWidth); siteSearch.classList.add('search'); siteSearch.innerHTML = `
    No matching pages
    `; const overlay = siteSearch.querySelector('.search__overlay'); const dialog = siteSearch.querySelector('.search__dialog'); const input = siteSearch.querySelector('.search__input'); const clearButton = siteSearch.querySelector('.search__clear-button'); const results = siteSearch.querySelector('.search__results'); const version = document.documentElement.getAttribute('data-shoelace-version'); const key = `search_${version}`; const searchDebounce = 50; const animationDuration = 150; let isShowing = false; let searchTimeout; let searchIndex; let map; const loadSearchIndex = new Promise(resolve => { const cache = localStorage.getItem(key); const wait = 'requestIdleCallback' in window ? requestIdleCallback : requestAnimationFrame; // Cleanup older search indices (everything before this version) try { const items = { ...localStorage }; Object.keys(items).forEach(k => { if (key > k) { localStorage.removeItem(k); } }); } catch { /* do nothing */ } // Look for a cached index try { if (cache) { const data = JSON.parse(cache); searchIndex = window.lunr.Index.load(data.searchIndex); map = data.map; return resolve(); } } catch { /* do nothing */ } // Wait until idle to fetch the index wait(() => { fetch('/assets/search.json') .then(res => res.json()) .then(data => { if (!window.lunr) { console.error('The Lunr search client has not yet been loaded.'); } searchIndex = window.lunr.Index.load(data.searchIndex); map = data.map; // Cache the search index for this version if (version) { try { localStorage.setItem(key, JSON.stringify(data)); } catch (err) { console.warn(`Unable to cache the search index: ${err}`); } } resolve(); }); }); }); async function show() { isShowing = true; document.body.append(siteSearch); document.body.classList.add('search-visible'); document.body.style.setProperty('--docs-search-scroll-lock-size', `${scrollbarWidth}px`); clearButton.hidden = true; requestAnimationFrame(() => input.focus()); updateResults(); dialog.showModal(); await Promise.all([ dialog.animate( [ { opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' }, { opacity: 1, transform: 'scale(1)', transformOrigin: 'top' } ], { duration: animationDuration } ).finished, overlay.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animationDuration }).finished ]); dialog.addEventListener('mousedown', handleMouseDown); dialog.addEventListener('keydown', handleKeyDown); } async function hide() { isShowing = false; await Promise.all([ dialog.animate( [ { opacity: 1, transform: 'scale(1)', transformOrigin: 'top' }, { opacity: 0, transform: 'scale(.9)', transformOrigin: 'top' } ], { duration: animationDuration } ).finished, overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: animationDuration }).finished ]); dialog.close(); input.blur(); // otherwise Safari will scroll to the bottom of the page on close input.value = ''; document.body.classList.remove('search-visible'); document.body.style.removeProperty('--docs-search-scroll-lock-size'); siteSearch.remove(); updateResults(); dialog.removeEventListener('mousedown', handleMouseDown); dialog.removeEventListener('keydown', handleKeyDown); } function handleInput() { clearButton.hidden = input.value === ''; // Debounce search queries clearTimeout(searchTimeout); searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce); } function handleClear() { clearButton.hidden = true; input.value = ''; input.focus(); updateResults(); } function handleMouseDown(event) { if (!event.target.closest('.search__content')) { hide(); } } function handleKeyDown(event) { // Close when pressing escape if (event.key === 'Escape') { event.preventDefault(); // prevent from closing immediately so it can animate event.stopImmediatePropagation(); hide(); return; } // Handle keyboard selections if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) { event.preventDefault(); const currentEl = results.querySelector('[data-selected="true"]'); const items = [...results.querySelectorAll('li')]; const index = items.indexOf(currentEl); let nextEl; if (items.length === 0) { return; } switch (event.key) { case 'ArrowUp': nextEl = items[Math.max(0, index - 1)]; break; case 'ArrowDown': nextEl = items[Math.min(items.length - 1, index + 1)]; break; case 'Home': nextEl = items[0]; break; case 'End': nextEl = items[items.length - 1]; break; case 'Enter': currentEl?.querySelector('a')?.click(); break; } // Update the selected item items.forEach(item => { if (item === nextEl) { input.setAttribute('aria-activedescendant', item.id); item.setAttribute('data-selected', 'true'); nextEl.scrollIntoView({ block: 'nearest' }); } else { item.setAttribute('data-selected', 'false'); } }); } } async function updateResults(query = '') { try { await loadSearchIndex; const hasQuery = query.length > 0; const searchTerms = query .split(' ') .map((term, index, arr) => { // Search API: https://lunrjs.com/guides/searching.html if (index === arr.length - 1) { // The last term is not mandatory and 1x fuzzy. We also duplicate it with a wildcard to match partial words // as the user types. return `${term}~1 ${term}*`; } else { // All other terms are mandatory and 1x fuzzy return `+${term}~1`; } }) .join(' '); const matches = hasQuery ? searchIndex.search(searchTerms) : []; const hasResults = hasQuery && matches.length > 0; siteSearch.classList.toggle('search--has-results', hasQuery && hasResults); siteSearch.classList.toggle('search--no-results', hasQuery && !hasResults); input.setAttribute('aria-activedescendant', ''); results.innerHTML = ''; matches.forEach((match, index) => { const page = map[match.ref]; const li = document.createElement('li'); const a = document.createElement('a'); const displayTitle = page.title ?? ''; const displayDescription = page.description ?? ''; const displayUrl = page.url.replace(/^\//, '').replace(/\/$/, ''); let icon = 'file-text'; a.setAttribute('role', 'option'); a.setAttribute('id', `search-result-item-${match.ref}`); if (page.url.includes('getting-started/')) { icon = 'lightbulb'; } if (page.url.includes('resources/')) { icon = 'book'; } if (page.url.includes('components/')) { icon = 'puzzle'; } if (page.url.includes('tokens/')) { icon = 'palette2'; } if (page.url.includes('utilities/')) { icon = 'wrench'; } if (page.url.includes('tutorials/')) { icon = 'joystick'; } li.classList.add('search__result'); li.setAttribute('role', 'option'); li.setAttribute('id', `search-result-item-${match.ref}`); li.setAttribute('data-selected', index === 0 ? 'true' : 'false'); a.href = page.url; a.innerHTML = `
    `; a.querySelector('.search__result-title').textContent = displayTitle; a.querySelector('.search__result-description').textContent = displayDescription; a.querySelector('.search__result-url').textContent = displayUrl; li.appendChild(a); results.appendChild(li); }); } catch { // Ignore query errors as the user types } } // Show the search dialog when clicking on data-plugin="search" document.addEventListener('click', event => { const searchButton = event.target.closest('[data-plugin="search"]'); if (searchButton) { show(); } }); // Show the search dialog when slash (or CMD+K) is pressed and focus is not inside a form element document.addEventListener('keydown', event => { if ( !isShowing && (event.key === '/' || (event.key === 'k' && (event.metaKey || event.ctrlKey))) && !event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase())) ) { event.preventDefault(); show(); } }); // Purge cache when we press CMD+CTRL+R document.addEventListener('keydown', event => { if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'r') { localStorage.clear(); } }); input.addEventListener('input', handleInput); clearButton.addEventListener('click', handleClear); // Close when a result is selected results.addEventListener('click', event => { if (event.target.closest('a')) { hide(); } }); })();