]> BookStack Code Mirror - website/commitdiff
Integrated new webidx system into layout/design
authorDan Brown <redacted>
Sun, 28 Jan 2024 16:49:49 +0000 (16:49 +0000)
committerDan Brown <redacted>
Sun, 28 Jan 2024 16:49:49 +0000 (16:49 +0000)
themes/bookstack/layouts/partials/footer.html
themes/bookstack/layouts/partials/header.html
themes/bookstack/sass/styles.scss
themes/bookstack/static/js/script.js
themes/bookstack/static/libs/webidx.js

index 4068d393ecec0748f6748d3486a1ef65393de229..951dab836e91d4c60d178f8fbb2e3d95a4edae06 100644 (file)
         </div>
     </footer>
 
-    <form onsubmit="window.webidx.search({dbfile:'/search.db',query:document.getElementById('q').value});return false;">
-    <input id="q" type="search">
-    </form>
-
     <script src="{{.Site.BaseURL}}libs/sql-wasm.js"></script>
     <script src="{{.Site.BaseURL}}libs/webidx.js"></script>
 
index 7d2189e89b4d39ac7d188444f62754803f1fa502..557054a6d657468faf133fc4e28c6766532d174e 100644 (file)
@@ -76,8 +76,6 @@
 
     <script async defer data-domain="bookstackapp.com" src="https://analytics.bookstackapp.com/js/plausible.js"></script>
 
-    <link rel="stylesheet" type="text/css" href="{{.Site.BaseURL}}libs/docs-searchbar.min.css" />
-
     {{ if .Site.Params.customHeaderPartial }}
         {{ partial .Site.Params.customHeaderPartial . }}
     {{ end }}
           <button tabindex="1" id="menu-button" class="button muted" type="button">{{partial "icon/menu.svg"}}</button>
       </div>
       <div class="header-search-section">
-        <input type="text" placeholder="Search site" class="doc-search-input">
+        <form id="site-search-form">
+          <input id="site-search-input" type="search" placeholder="Search site" value="">
+          <dialog>
+          </dialog>
+        </form>
       </div>
       <div class="main-nav">
           <div class="nav-dropdown-wrap">
index a5bb0eb3f367d372fdc93d7b4510c0e6a7b2a6b9..439a8b99b915f690b5e26740553959703a0d8aab 100644 (file)
@@ -359,15 +359,16 @@ h2.thin-margin {
 }
 
 
-input[type="text"].doc-search-input {
+header input[type="search"] {
   background-color: transparent;
   border: 2px solid #FFF;
   color: #FFF;
   background-color: transparent;
   border-radius: 30px;
-  width: 300px;
-  margin-bottom: $-m;
-  margin-top: $-xs;
+  width: 280px;
+  margin-block: $-m;
+  font-size: 14px;
+  padding: 7.2px 12px;
   &::-webkit-input-placeholder { /* Chrome */
     color: rgba(255, 255, 255, 0.7);
   }
@@ -383,6 +384,102 @@ input[type="text"].doc-search-input {
     outline: none;
   }
 }
+#site-search-form {
+  position: relative;
+}
+#site-search-form dialog {
+  position: absolute;
+  left: -10px;
+  top: 100%;
+  margin-top: -14px;
+  border: 0;
+  margin-inline: 0;
+  background-color: #FFF;
+  box-shadow: 0 2px 12px 1px rgba(0, 0, 0, 0.2);
+  border-radius: 4px;
+  width: 300px;
+  padding: 0;
+  min-height: 42px;
+  a {
+    display: block;
+    padding: 0.25rem 1rem 0.25rem 2rem;
+    border-bottom: 1px solid #EEE;
+    color: #222;
+    font-size: 15px;
+  }
+  a:hover {
+    color: $primary;
+    text-decoration: none;
+    background-color: rgba($primary, 0.1);
+  }
+  a:last-child {
+    border-bottom: 0;
+  }
+  strong.search-category-title {
+    padding: .5rem 1rem .25rem 1rem;
+    color: #666;
+    display: block;
+    font-size: 14px;
+  }
+}
+
+// Loader from https://loading.io/css/
+.lds-ellipsis {
+  display: block;
+  position: relative;
+  width: 80px;
+  height: 80px;
+  margin: 0 auto;
+}
+.lds-ellipsis div {
+  position: absolute;
+  top: 33px;
+  width: 13px;
+  height: 13px;
+  border-radius: 50%;
+  background: $primary;
+  animation-timing-function: cubic-bezier(0, 1, 1, 0);
+}
+.lds-ellipsis div:nth-child(1) {
+  left: 8px;
+  animation: lds-ellipsis1 0.6s infinite;
+}
+.lds-ellipsis div:nth-child(2) {
+  left: 8px;
+  animation: lds-ellipsis2 0.6s infinite;
+}
+.lds-ellipsis div:nth-child(3) {
+  left: 32px;
+  animation: lds-ellipsis2 0.6s infinite;
+}
+.lds-ellipsis div:nth-child(4) {
+  left: 56px;
+  animation: lds-ellipsis3 0.6s infinite;
+}
+@keyframes lds-ellipsis1 {
+  0% {
+    transform: scale(0);
+  }
+  100% {
+    transform: scale(1);
+  }
+}
+@keyframes lds-ellipsis3 {
+  0% {
+    transform: scale(1);
+  }
+  100% {
+    transform: scale(0);
+  }
+}
+@keyframes lds-ellipsis2 {
+  0% {
+    transform: translate(0, 0);
+  }
+  100% {
+    transform: translate(24px, 0);
+  }
+}
 
 .youtube-embed-wrap {
   margin-bottom: 1rem;
index 1e4d51db29e1034a08dee0f5593da5da709efc5b..8fe5d0fb4438c2c4c6499ab36cae76fcb79db7e7 100644 (file)
@@ -10,8 +10,11 @@ menuButton.addEventListener('click', function(event) {
 });
 
 document.body.addEventListener('click', function(event) {
-  menuDropDown.classList.remove('showing');
-  event.stopPropagation();  
+  const isShown = menuDropDown.classList.contains('showing');
+  if (isShown) {
+    menuDropDown.classList.remove('showing');
+    event.stopPropagation();  
+  }
 });
 
 
@@ -32,4 +35,145 @@ document.body.addEventListener('dblclick', event => {
   if (isHeader && event.target.id) {
     window.location.hash = event.target.id;
   }
-});
\ No newline at end of file
+});
+
+function el(tag, attributes = {}, children = []) {
+  const elem = document.createElement(tag);
+  for (const attr of Object.keys(attributes)) {
+    elem.setAttribute(attr, attributes[attr]);
+  }
+  for (let child of children) {
+    if (typeof child === 'string') {
+      child = new Text(child);
+    }
+    elem.append(child);
+  }
+  return elem;
+}
+
+// Site search
+const searchForm = document.getElementById('site-search-form');
+const searchInput = document.getElementById('site-search-input');
+const searchDialog = searchForm.querySelector('dialog');
+
+async function runSearch() {
+  const searchTerm = searchInput.value.toLowerCase();
+
+  let pages = [];
+  try {
+    pages = await window.webidx.search({
+      dbfile:'/search.db',
+      query: searchTerm,
+    });
+  } catch (error) {
+    searchDialog.innerHTML = '<strong class="search-category-title">Failed to load search results</strong>';
+    console.error(error);
+    return;
+  }
+
+  // Sort pages to prioritise those with word in title
+  pages.sort((a, b) => {
+    const aScore = (a.url.includes(searchTerm) || a.title.toLowerCase().includes(searchTerm)) ? 1 : 0;
+    const bScore = (b.url.includes(searchTerm) || b.title.toLowerCase().includes(searchTerm)) ? 1 : 0;
+    return bScore - aScore;
+  });
+
+  // Categorizes pages to display
+  const categorised = {
+    docs: {title: 'Documentation', filter: '/docs/', pages: []},
+    hacks: {title: 'Hacks', filter: '/hacks/', pages: []}, 
+    blog: {title: 'From the blog', filter: '/blog/', pages: []}, 
+    other: {title: 'Site Pages', filter: '', pages: []},
+  };
+  const categoryNames = Object.keys(categorised);
+
+  for (const page of pages) {
+    for (const categoryName of categoryNames) {
+      const category = categorised[categoryName];
+      if (page.url.includes(category.filter)) {
+        category.pages.push(page);
+        break;
+      }
+    }
+  }
+
+  const categoryResults = categoryNames.map(name => {
+    const category = categorised[name];
+    if (category.pages.length === 0) {
+      return null;
+    }
+    return el('div', {}, [
+      el('strong', {class: 'search-category-title'}, [category.title]),
+      el('div', {}, category.pages.slice(0, 5).map(page => {
+        return el('a', {href: page.url}, [page.title]);
+      })),
+    ]);
+  }).filter(Boolean);
+
+  const emptyResult = el('strong', {class: 'search-category-title'}, [el('em', {}, 'No results found')]);
+  const resultList = categoryResults.length ? categoryResults : [emptyResult];
+  const results = el('div', {}, resultList);
+
+  for (const child of searchDialog.children) {
+    child.remove();
+  }
+  
+  searchDialog.append(results);
+  showSearchDialog();
+}
+
+function showSearchDialog() {
+  if (searchDialog.open) {
+    return;
+  }
+  searchDialog.show();
+  searchInput.focus();
+  
+  const clickListener = e => {
+    if(!e.target.closest('dialog')) {
+      closeListener();
+    }
+  };
+
+  const escListener = e => {
+    if (e.key === 'Escape') {
+        closeListener();
+    }
+  };
+
+  const mouseLeaveListener = e => {
+    closeListener();
+  }
+
+  const closeListener = () => {
+    searchDialog.close();
+    document.removeEventListener('click', clickListener);
+    document.removeEventListener('keydown', escListener);
+    searchForm.removeEventListener('mouseleave', mouseLeaveListener);
+  };
+
+  document.addEventListener('click', clickListener);
+  document.addEventListener('keydown', escListener);
+  searchForm.addEventListener('mouseleave', mouseLeaveListener);
+}
+
+function showSearchLoading() {
+  searchDialog.innerHTML = `<div class="lds-ellipsis"><div></div><div></div><div></div><div></div></div>`;
+  showSearchDialog();
+}
+
+searchForm.addEventListener('submit', event => {
+  event.preventDefault();
+  showSearchLoading();
+  runSearch();
+});
+
+searchInput.addEventListener('input', event => {
+  const termLength = searchInput.value.length;
+  if (termLength === 0) {
+    searchDialog.close();
+  } else if (termLength > 2) {
+    showSearchLoading();
+    runSearch();
+  }
+})
\ No newline at end of file
index d261abb24c7ce726ab8d3127b1808d4830dddb39..0ca75ecbabda28b4e51efd7df67d5ab336492e8e 100644 (file)
@@ -2,9 +2,10 @@
 // BSD 3-Clause License
 // Copyright (c) 2024, Gavin Brown
 // Full license: https://github.com/gbxyz/webidx/blob/a28a984d38fd546d1bec4d6a4a5a47ab86cb08f8/LICENSE
+// Modified since copied.
 
 window.webidx = {};
-webidx = window.webidx;
+const webidx = window.webidx;
 
 webidx.search = async function (params) {
   if (!webidx.sql) {
@@ -15,33 +16,42 @@ webidx.search = async function (params) {
   }
 
   if (webidx.hasOwnProperty('db')) {
-    webidx.displayResults(webidx.query(params.query), params);
-
+    return webidx.query(params.query);
+  } else if (webidx.loadingPromise) {
+    await webidx.loadingPromise;
+    await new Promise((res, rej) => setTimeout(res, 10));
+    return webidx.query(params.query);
   } else {
-    webidx.loadDB(params);
+    webidx.loadingPromise = webidx.loadDB(params);
+
+    webidx.loadingPromise.then(() => {
+      webidx.loadingPromise = null;
+    });
 
+    return webidx.loadingPromise
   }
 };
 
 webidx.loadDB = function (params) {
-  var xhr = new XMLHttpRequest();
+  return new Promise((res, rej) => {
+    var xhr = new XMLHttpRequest();
 
-  xhr.open('GET', params.dbfile);
-  xhr.timeout = params.timeout ?? 5000;
-  xhr.responseType = 'arraybuffer';
+    xhr.open('GET', params.dbfile);
+    xhr.timeout = params.timeout ?? 5000;
+    xhr.responseType = 'arraybuffer';
 
-  xhr.ontimeout = function() {
-    if (params.hasOwnProperty('errorCallback')) {
-      params.errorCallback('Unable to load index, please refresh the page.');
-    }
-  };
+    xhr.ontimeout = function() {
+      rej('Unable to load index, please refresh the page.');
+    };
 
-  xhr.onload = function() {
-    webidx.initializeDB(this.response);
-    webidx.displayResults(webidx.query(params.query), params);
-  };
+    xhr.onload = function() {
+      webidx.initializeDB(this.response);
+      const results = webidx.query(params.query);
+      res(results);
+    };
 
-  xhr.send();
+    xhr.send();
+  });
 };
 
 webidx.initializeDB = function (arrayBuffer) {