pagefind integration and search bar
This commit is contained in:
parent
a787497feb
commit
cd1226e333
19 changed files with 565 additions and 240 deletions
12
src/components/iconsModule.ts
Normal file
12
src/components/iconsModule.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
const magnifier =
|
||||
"M232.49 215.51L185 168a92.12 92.12 0 1 0-17 17l47.53 47.54a12 12 0 0 0 17-17ZM44 112a68 68 0 1 1 68 68a68.07 68.07 0 0 1-68-68";
|
||||
|
||||
function getHTMLIcon(name: string): string {
|
||||
const hashmap = {
|
||||
magnifier: () => magnifier,
|
||||
};
|
||||
|
||||
return hashmap[name as keyof typeof hashmap]();
|
||||
}
|
||||
|
||||
export { getHTMLIcon };
|
|
@ -1,7 +1,154 @@
|
|||
---
|
||||
import SearchComponent from "./search";
|
||||
import "@pagefind/default-ui/css/ui.css";
|
||||
import { getHTMLIcon } from "../iconsModule";
|
||||
|
||||
const magnifier = getHTMLIcon("magnifier");
|
||||
---
|
||||
|
||||
<div class="search-wrapper">
|
||||
<SearchComponent client:only={"solid-js"}/>
|
||||
</div>
|
||||
<site-search class="search-wrapper">
|
||||
<button
|
||||
data-open-modal
|
||||
disabled
|
||||
aria-label="Search"
|
||||
aria-keyshortcuts="Control+K"
|
||||
class="search-button"
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><path fill="currentColor" d={magnifier}/></svg>
|
||||
<span class="search-label" aria-hidden="true">Search</span>
|
||||
<kbd class="search-kbd">
|
||||
<kbd>Ctrl</kbd><kbd>K</kbd>
|
||||
</kbd>
|
||||
</button>
|
||||
<dialog aria-label="Search" class="search-dialog">
|
||||
<div class="dialog-frame">
|
||||
<button data-close-modal class="search-cancel">
|
||||
Cancel
|
||||
</button>
|
||||
<div class="search-container">
|
||||
<div id="qs_search"/>
|
||||
</div>
|
||||
</div>
|
||||
</dialog>
|
||||
</site-search>
|
||||
{
|
||||
/**
|
||||
* NOTE: YOINKED FROM STARLIGHT
|
||||
* This is intentionally inlined to avoid briefly showing an invalid shortcut.
|
||||
* Purposely using the deprecated `navigator.platform` property to detect Apple devices, as the
|
||||
* user agent is spoofed by some browsers when opening the devtools.
|
||||
*/
|
||||
}
|
||||
<script is:inline>
|
||||
(() => {
|
||||
const openBtn = document.querySelector('button[data-open-modal]');
|
||||
const shortcut = openBtn?.querySelector('kbd');
|
||||
if (!openBtn || !(shortcut instanceof HTMLElement)) return;
|
||||
const platformKey = shortcut.querySelector('kbd');
|
||||
if (platformKey && /(Mac|iPhone|iPod|iPad)/i.test(navigator.platform)) {
|
||||
platformKey.textContent = '⌘';
|
||||
openBtn.setAttribute('aria-keyshortcuts', 'Meta+K');
|
||||
}
|
||||
shortcut.style.display = '';
|
||||
})();
|
||||
</script>
|
||||
|
||||
<script>
|
||||
import { getQMLTypeLinkObject, getQMLTypeLink, getIconForLink } from '@src/config/io/helpers';
|
||||
|
||||
class SiteSearch extends HTMLElement {
|
||||
constructor() {
|
||||
super();
|
||||
const openBtn = this.querySelector<HTMLButtonElement>('button[data-open-modal]')!;
|
||||
const closeBtn = this.querySelector<HTMLButtonElement>('button[data-close-modal]')!;
|
||||
const dialog = this.querySelector('dialog')!;
|
||||
const dialogFrame = this.querySelector('.dialog-frame')!;
|
||||
|
||||
/** Close the modal if a user clicks on a link or outside of the modal. */
|
||||
const onClick = (event: MouseEvent) => {
|
||||
const isLink = 'href' in (event.target || {});
|
||||
if (
|
||||
isLink ||
|
||||
(document.body.contains(event.target as Node) &&
|
||||
!dialogFrame.contains(event.target as Node))
|
||||
) {
|
||||
closeModal();
|
||||
}
|
||||
};
|
||||
|
||||
const openModal = (event?: MouseEvent) => {
|
||||
dialog.showModal();
|
||||
document.body.toggleAttribute('data-search-modal-open', true);
|
||||
this.querySelector('input')?.focus();
|
||||
event?.stopPropagation();
|
||||
window.addEventListener('click', onClick);
|
||||
};
|
||||
|
||||
const closeModal = () => dialog.close();
|
||||
|
||||
openBtn.addEventListener('click', openModal);
|
||||
openBtn.disabled = false;
|
||||
closeBtn.addEventListener('click', closeModal);
|
||||
|
||||
dialog.addEventListener('close', () => {
|
||||
document.body.toggleAttribute('data-search-modal-open', false);
|
||||
window.removeEventListener('click', onClick);
|
||||
});
|
||||
|
||||
// Listen for `ctrl + k` and `cmd + k` keyboard shortcuts.
|
||||
window.addEventListener('keydown', (e) => {
|
||||
if ((e.metaKey === true || e.ctrlKey === true) && e.key === 'k') {
|
||||
dialog.open ? closeModal() : openModal();
|
||||
e.preventDefault();
|
||||
}
|
||||
});
|
||||
|
||||
const processExcerpt = (sub_resultExcerpt:string):string => {
|
||||
const linkRegex = /TYPE99(\w+.)99TYPE/g;
|
||||
let excerpt = sub_resultExcerpt;
|
||||
const match = [...excerpt.matchAll(linkRegex)];
|
||||
if (match.length > 0){
|
||||
for (const matching of match) {
|
||||
const linkObject = getQMLTypeLinkObject(matching[1]);
|
||||
const link = getQMLTypeLink(linkObject);
|
||||
const icon = linkObject.mtype ? getIconForLink(linkObject.mtype, false) : null;
|
||||
|
||||
// for signal
|
||||
const bracketString = getIconForLink("func", false)
|
||||
|
||||
const newLink = `<span class="type${linkObject.mtype}-link typedata-link">${icon ? icon : ""}<a href=${link}>${linkObject.mname || linkObject.name}</a>${linkObject.mtype === "signal" ? bracketString : ""}</span>`;
|
||||
excerpt = excerpt.replace(matching[0], newLink)
|
||||
}
|
||||
}
|
||||
return excerpt
|
||||
}
|
||||
|
||||
|
||||
const formatURL = (path: string) => path;
|
||||
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const onIdle = window.requestIdleCallback || ((cb) => setTimeout(cb, 1));
|
||||
onIdle(async () => {
|
||||
// @ts-expect-error — Missing types for @pagefind/default-ui package.
|
||||
const { PagefindUI } = await import('@pagefind/default-ui');
|
||||
new PagefindUI({
|
||||
element: '#qs_search',
|
||||
baseUrl: import.meta.env.BASE_URL,
|
||||
bundlePath: import.meta.env.BASE_URL.replace(/\/$/, '') + '/pagefind/',
|
||||
showImages: false,
|
||||
showSubResults: true,
|
||||
processResult: (result: { url: string; excerpt:string; sub_results: Array<{ url: string, excerpt:string }> }) => {
|
||||
result.url = formatURL(result.url);
|
||||
result.sub_results = result.sub_results.map((sub_result) => {
|
||||
sub_result.url = formatURL(sub_result.url);
|
||||
sub_result.excerpt = processExcerpt(sub_result.excerpt)
|
||||
return sub_result;
|
||||
});
|
||||
result.excerpt = processExcerpt(result.excerpt)
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
customElements.define('site-search', SiteSearch);
|
||||
</script>
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import { For, type Component } from "solid-js";
|
||||
import type { SearchResult } from "./types";
|
||||
import {
|
||||
getIconForLink,
|
||||
getQMLTypeLink,
|
||||
getQMLTypeLinkObject,
|
||||
} from "@src/config/io/helpers";
|
||||
|
||||
const SearchModal: Component<{
|
||||
results: SearchResult[];
|
||||
}> = props => {
|
||||
const { results } = props;
|
||||
const linkRegex = /TYPE99(\w+.)99TYPE/g;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="search-modal"
|
||||
class="search-output"
|
||||
>
|
||||
<For each={results}>
|
||||
{result => {
|
||||
let excerpt = result.excerpt;
|
||||
const linkMatch = [...excerpt.matchAll(linkRegex)];
|
||||
for (const match of linkMatch) {
|
||||
const unparsed = match[1];
|
||||
const linkObject = getQMLTypeLinkObject(unparsed);
|
||||
const linkParsed = getQMLTypeLink(linkObject);
|
||||
const icon = linkObject.mtype
|
||||
? getIconForLink(linkObject.mtype, false)
|
||||
: "";
|
||||
const bracketString = getIconForLink("func", false);
|
||||
const newString = `<span class="type${linkObject.mtype}-link typedata-link">${icon}<a href=${linkParsed}>${linkObject.mname || linkObject.name}</a>${linkObject.mtype === "signal" ? bracketString : ""}</span>`;
|
||||
excerpt = excerpt.replace(match[0], newString);
|
||||
}
|
||||
excerpt = `${excerpt}...`;
|
||||
return (
|
||||
<div class="search-output_item">
|
||||
<h3 class="search-output_heading">
|
||||
<a href={result.url}>{result.meta.title}</a>
|
||||
</h3>
|
||||
<section
|
||||
class="search-output_excerpt"
|
||||
innerHTML={excerpt}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchModal;
|
|
@ -1,61 +0,0 @@
|
|||
import {
|
||||
createResource,
|
||||
createSignal,
|
||||
type Component,
|
||||
} from "solid-js";
|
||||
|
||||
import type { SearchResult } from "./types";
|
||||
import SearchModal from "./SearchModal";
|
||||
|
||||
//const pagefindPath = "@dist/pagefind/pagefind.js";
|
||||
//const pagefind = await import(pagefindPath);
|
||||
const pagefind = await import("@dist/pagefind/pagefind.js");
|
||||
pagefind.init();
|
||||
|
||||
async function PagefindSearch(query: string) {
|
||||
const search = await pagefind.search(query);
|
||||
const resultdata: SearchResult[] = [];
|
||||
for (const result of search.results) {
|
||||
const data = await result.data();
|
||||
resultdata.push(data);
|
||||
}
|
||||
return resultdata;
|
||||
}
|
||||
|
||||
const SearchComponent: Component = () => {
|
||||
let modal!: HTMLElement;
|
||||
const [query, setQuery] = createSignal("");
|
||||
const [results, { refetch }] = createResource(
|
||||
query,
|
||||
PagefindSearch
|
||||
);
|
||||
|
||||
function handleSearch(value: string) {
|
||||
setQuery(value);
|
||||
refetch();
|
||||
console.log(results());
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="search">
|
||||
<input
|
||||
id="search-input"
|
||||
type="text"
|
||||
role="searchbox"
|
||||
incremental
|
||||
value={query()}
|
||||
placeholder="Search"
|
||||
onChange={e => handleSearch(e.target.value)}
|
||||
//onfocusout={() => setQuery("")}
|
||||
/>{" "}
|
||||
{!results.loading && results() && results()!.length > 0 ? (
|
||||
<SearchModal
|
||||
results={results()!}
|
||||
ref={modal}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default SearchComponent;
|
15
src/components/navigation/search/types.d.ts
vendored
15
src/components/navigation/search/types.d.ts
vendored
|
@ -1,15 +0,0 @@
|
|||
interface SearchResult {
|
||||
url: string;
|
||||
excerpt: string;
|
||||
meta: {
|
||||
title: string;
|
||||
image?: string;
|
||||
};
|
||||
sub_results: {
|
||||
title: string;
|
||||
url: string;
|
||||
excerpt: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export type { SearchResult }
|
|
@ -37,7 +37,10 @@ const NavComponent: Component<NavProps> = props => {
|
|||
<XToMenu class="nav-icon" />
|
||||
)}
|
||||
</div>
|
||||
<div class={`nav-items ${open() ? "shown" : ""}`}>
|
||||
<div
|
||||
id={open() ? "#qs_search" : ""}
|
||||
class={`nav-items ${open() ? "shown" : ""}`}
|
||||
>
|
||||
<Tree
|
||||
currentRoute={tree.currentRoute}
|
||||
currentModule={tree.currentModule}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue