]> git.ardis.lu - ardislu.dev/commitdiff
Add search
authorArdis Lu <ardisglu@gmail.com>
Mon, 15 Aug 2022 05:37:24 +0000 (22:37 -0700)
committerArdis Lu <ardisglu@gmail.com>
Mon, 15 Aug 2022 05:37:24 +0000 (22:37 -0700)
index.html
script.js
style.css

index 29d10a12af9476ddce6803fd6902922077ef3741..f12768af23d035ae7f5eca9606c6aa0b6cd1e26c 100644 (file)
         <path d="m0.96453 17.951 1.1832-0.981 5.9161 4.904 5.9162-4.904v1.962l-5.9162 4.904z" />
       </svg>
     </a>
-    <h1>ardislu.dev</h1>
+
+    <form role="search">
+      <input type="search" name="query">
+      <button>
+        <svg role="img" aria-labelledby="search-icon" width="24" height="24" viewBox="0 0 24 24" fill="none">
+          <title id="search-icon">Search this site</title>
+          <circle cx="9" cy="9" r="7" stroke="#000" />
+          <line x1="14" y1="14" x2="22" y2="22" stroke="#000" stroke-linecap="round" />
+        </svg>
+      </button>
+    </form>
+
     <nav>
       <a id="edit" href="/">
         <svg role="img" aria-labelledby="pencil" width="24" height="24" viewBox="0 0 24 24">
index 2c3036750f091785c4a4a5fbb7b830f2a5fd0a30..96e005cbe5a1d8bbbc3c52fb9328b7ab4a6a0231 100644 (file)
--- a/script.js
+++ b/script.js
@@ -1,3 +1,4 @@
+import FlexDocument from 'https://cdn.jsdelivr.net/gh/nextapps-de/flexsearch@0.7.2/dist/module/document.min.js';
 import { marked } from 'https://cdn.jsdelivr.net/npm/marked@4.0.18/lib/marked.esm.min.js';
 import hljs from 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.6.0/build/es/highlight.min.js';
 import powershell from 'https://cdn.jsdelivr.net/gh/highlightjs/cdn-release@11.6.0/build/es/languages/powershell.min.js';
@@ -122,6 +123,69 @@ function setPageMetadata(values) {
   document.querySelector('#edit').href = values.editUrl;
 }
 
+/* Create the flexsearch index for searching. */
+function buildFlexIndex(metadata) {
+  const index = new FlexDocument({
+    index: ['title', 'description', 'tags', 'path'],
+    store: ['path'],
+    tokenize: 'full'
+  });
+
+  for (const [path, post] of metadata) {
+    index.add({
+      path: path,
+      ...post
+    });
+  }
+
+  return index;
+}
+
+/* Wrapper to search the flexsearch index and return a filtered metadata map. */
+function search(str) {
+  const index = globalThis.flexIndex;
+  const search = index.search(str, { enrich: true });
+  const paths = new Set(search.map(j => j.result.map(k => k.doc.path)).flat());
+  const filteredMetadata = new Map();
+  for (const path of paths) {
+    filteredMetadata.set(path, globalThis.metadata.get(path));
+  }
+  return filteredMetadata;
+}
+globalThis.search = search;
+
+/* On search submit, replace <main> with the search results. */
+document.querySelector('form').addEventListener('submit', async event => {
+  event.preventDefault();
+
+  const data = new FormData(event.target);
+  const query = data.get('query')
+  if (query === '') {
+    document.querySelector('main').replaceWith(globalThis.components.get('home'));
+    return;
+  }
+
+  const filteredMetadata = search(query);
+  const filteredHome = buildHomeComponent(filteredMetadata);
+
+  // TODO: update the query params to support deeplinks
+  // TODO: logic to handle searching while in an article
+  document.querySelector('main').replaceWith(filteredHome);
+});
+
+/* Focus the search input on '/' or Ctrl + k. */
+document.addEventListener('keydown', event => {
+  const search = document.querySelector('input');
+  const ctrlOrMetaPressed = event.ctrlKey || event.metaKey;
+  const isSlash = event.key === '/' && !ctrlOrMetaPressed;
+  const isCtrlK = event.key === 'k' && ctrlOrMetaPressed;
+  const isTextField = ['TEXTAREA', 'INPUT'].includes(event.target.tagName);
+  if ((isSlash || isCtrlK) && !isTextField && document.activeElement !== search) {
+    event.preventDefault();
+    search.focus();
+  }
+});
+
 /* Implement client-side routing. Handles view logic for /home and /:post routes. */
 async function showPage(path) {
   // Determine which placeholders to show
@@ -135,6 +199,7 @@ async function showPage(path) {
   // Load all post metadata if it hasn't been loaded yet
   if (globalThis.metadata.size === 0) {
     globalThis.metadata = await fetchPostMetadata();
+    globalThis.flexIndex = buildFlexIndex(globalThis.metadata);
   }
 
   if (path === '/home' || path === '/') {
@@ -223,6 +288,7 @@ function help() {
   console.log(`%c ${ascii}`, 'font-weight: bold')
   console.table({
     'help()': 'Print this message.',
+    'search(str)': 'Search the metadata.',
     'metadata': 'Map object containing all blog post metadata and content.',
     'components': 'Map object containing live HTML elements of all the components used for the site.'
   });
index 77320482ceb1a1fc85b154b767ffbb9cf0a223b8..611f97cc535793adf87a81d134ee1c791581dcdb 100644 (file)
--- a/style.css
+++ b/style.css
@@ -14,6 +14,7 @@ header {
   align-items: center;
   justify-content: space-between;
   inline-size: min(100% - 2rem, 1440px);
+  padding-block: 1rem;
   margin-inline: auto;
   margin-block-end: 1rem;
   border-block-end: 1px hsl(216deg 100% 80%) solid;
@@ -27,12 +28,18 @@ a > svg {
   vertical-align: middle;
 }
 
+form > button {
+  background: transparent;
+  border: none;
+  cursor: pointer;
+}
+
 h1 {
   font-size: 2.25em;
   margin-block: 0.5em;
 }
 
-a {
+a, form > button {
   color: hsl(216deg 100% 45%);
   font-weight: bold;
   text-decoration-color: transparent;
@@ -40,7 +47,7 @@ a {
   transition: 0.15s;
 }
 
-a:hover, a:focus {
+a:hover, a:focus, form > button:hover, form > button:focus {
   background: hsl(30deg 80% 90%);
   border-radius: 0.25rem;
   text-decoration-color: currentColor;
This page took 0.054531 seconds and 4 git commands to generate.