custom search

This commit is contained in:
Cory LaViska
2021-09-07 11:52:58 -04:00
parent 8e209d2767
commit 03fb75f030
15 changed files with 505 additions and 32 deletions

1
.gitignore vendored
View File

@@ -1,6 +1,7 @@
.DS_Store
.cache
docs/dist
docs/search.json
dist
examples
node_modules

View File

@@ -1,3 +1,5 @@
# Not Found
Sorry, I couldn't find that page.
<img class="not-found-image" src="/assets/images/undraw-not-found.svg" alt="Cute monsters hiding behind a tree">
Sorry, I couldn't find that page. Have you tried pressing <kbd>/</kbd> to search?

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 6.8 KiB

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,125 @@
body.site-search-visible {
overflow: hidden;
}
.sidebar .search-box {
margin: 1.25rem 26px;
}
.sidebar .search-box kbd {
margin-top: 2px;
}
/* Site search */
.site-search {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 1000;
}
.site-search[hidden] {
display: none;
}
.site-search__overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgb(var(--sl-overlay-background-color) / var(--sl-overlay-opacity));
z-index: -1;
}
.site-search__panel {
display: flex;
flex-direction: column;
max-width: 460px;
max-height: calc(100vh - 20rem);
background-color: rgb(var(--sl-color-neutral-0));
border-radius: var(--sl-border-radius-large);
box-shadow: var(--sl-shadow-x-large);
margin: 10rem auto;
}
@media screen and (max-width: 1000px) {
.site-search__panel {
max-width: 100%;
max-height: 92vh;
margin: 4vh var(--sl-spacing-medium);
}
}
.site-search__input::part(base) {
border: none;
}
.site-search__input:focus-within::part(base) {
outline: none;
box-shadow: none;
}
.site-search__body {
flex: 1 1 auto;
overflow: auto;
}
.site-search--has-results .site-search__body {
border-top: solid 1px rgb(var(--sl-color-neutral-200));
}
.site-search__results {
display: none;
line-height: var(--sl-line-height-dense);
list-style: none;
padding: var(--sl-spacing-x-small) 0;
margin: 0;
}
.site-search--has-results .site-search__results {
display: block;
}
.site-search__results a {
display: block;
text-decoration: none;
padding: var(--sl-spacing-x-small) var(--sl-spacing-large);
}
.site-search__results li a:hover,
.site-search__results li a:hover small,
.site-search__results li a[aria-selected],
.site-search__results li a[aria-selected] small {
outline: none;
color: rgb(var(--sl-color-neutral-0));
background-color: rgb(var(--sl-color-primary-600));
}
.site-search__results h3 {
font-weight: var(--sl-font-weight-semibold);
margin: 0;
}
.site-search__results li {
padding: 0;
margin: 0;
}
.site-search__results small {
display: block;
color: rgb(var(--sl-color-neutral-600));
}
.site-search__empty {
display: none;
border-top: solid 1px rgb(var(--sl-color-neutral-200));
text-align: center;
padding: var(--sl-spacing-x-large);
}
.site-search--no-results .site-search__empty {
display: block;
}

View File

@@ -0,0 +1,266 @@
(() => {
if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.');
}
window.$docsify.plugins.push((hook, vm) => {
// Append the search box to the sidebar
hook.mounted(function () {
const appName = document.querySelector('.sidebar .app-name');
const searchBox = document.createElement('div');
searchBox.classList.add('search-box');
searchBox.innerHTML = `
<sl-input
type="search"
placeholder="Search"
clearable
pill
>
<sl-icon slot="prefix" name="search"></sl-icon>
<kbd slot="suffix" title="Press / to search">/</kbd>
</sl-input>
`;
const searchBoxInput = searchBox.querySelector('sl-input');
appName.insertAdjacentElement('afterend', searchBox);
// Show the search panel when the search is clicked
searchBoxInput.addEventListener('mousedown', event => {
event.preventDefault();
show();
});
// Show the search panel when a key is pressed
searchBoxInput.addEventListener('keydown', event => {
if (event.key === 'Tab') {
return;
}
// Pass the character that was typed through to the search input
if (event.key.length === 1) {
event.preventDefault();
input.value = event.key;
show();
}
});
});
// Append the search panel to the body
const siteSearch = document.createElement('div');
siteSearch.classList.add('site-search');
siteSearch.hidden = true;
siteSearch.innerHTML = `
<div class="site-search__overlay"></div>
<div class="site-search__panel">
<header class="site-search__header">
<sl-input
class="site-search__input"
type="search"
placeholder="Search this site"
size="large"
clearable
></sl-input>
</header>
<div class="site-search__body">
<ul class="site-search__results"></ul>
<div class="site-search__empty">No results found.</div>
</div>
</div>
`;
document.body.append(siteSearch);
const searchButtons = [...document.querySelectorAll('[data-site-search]')];
const overlay = siteSearch.querySelector('.site-search__overlay');
const panel = siteSearch.querySelector('.site-search__panel');
const input = siteSearch.querySelector('.site-search__input');
const results = siteSearch.querySelector('.site-search__results');
const animationDuration = 150;
const searchDebounce = 250;
let isShowing = false;
let searchTimeout;
let searchIndex;
let map;
// Load search data
const searchData = fetch('../../../search.json')
.then(res => res.json())
.then(data => {
searchIndex = lunr.Index.load(data.searchIndex);
map = data.map;
});
async function show() {
isShowing = true;
document.body.classList.add('site-search-visible');
siteSearch.hidden = false;
input.focus();
updateResults();
await Promise.all([
panel.animate(
[
{ opacity: 0, transform: 'scale(.9)' },
{ opacity: 1, transform: 'scale(1)' }
],
{ duration: animationDuration }
).finished,
overlay.animate([{ opacity: 0 }, { opacity: 1 }], { duration: animationDuration }).finished
]);
document.addEventListener('mousedown', handleDocumentMouseDown);
document.addEventListener('keydown', handleDocumentKeyDown);
document.addEventListener('focusin', handleDocumentFocusIn);
}
async function hide() {
isShowing = false;
document.body.classList.remove('site-search-visible');
await Promise.all([
panel.animate(
[
{ opacity: 1, transform: 'scale(1)' },
{ opacity: 0, transform: 'scale(.9)' }
],
{ duration: animationDuration }
).finished,
overlay.animate([{ opacity: 1 }, { opacity: 0 }], { duration: animationDuration }).finished
]);
siteSearch.hidden = true;
input.value = '';
updateResults();
document.removeEventListener('mousedown', handleDocumentMouseDown);
document.removeEventListener('keydown', handleDocumentKeyDown);
document.removeEventListener('focusin', handleDocumentFocusIn);
}
function handleInput() {
// Debounce search queries
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => updateResults(input.value), searchDebounce);
}
function handleDocumentFocusIn(event) {
// Close when focus leaves the panel
if (event.target.closest('.site-search__panel') !== panel) {
hide();
}
}
function handleDocumentMouseDown(event) {
// Close when clicking outside of the panel
if (event.target.closest('.site-search__overlay') === overlay) {
hide();
}
}
function handleDocumentKeyDown(event) {
// Close when pressing escape
if (event.key === 'Escape') {
event.preventDefault();
hide();
return;
}
// Handle keyboard selections
if (['ArrowDown', 'ArrowUp', 'Home', 'End', 'Enter'].includes(event.key)) {
event.preventDefault();
const currentEl = results.querySelector('[aria-selected]');
const items = [...results.querySelectorAll('a')];
let nextEl;
items.map(item => item.removeAttribute('aria-selected'));
if (items.length === 0) {
return;
}
const index = items.indexOf(currentEl);
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[items.length > 0 ? 1 : 0];
break;
case 'End':
nextEl = items[items.length - 1];
break;
case 'Enter':
(currentEl || items[0]).click();
break;
}
if (nextEl) {
nextEl.setAttribute('aria-selected', 'true');
nextEl.scrollIntoView({ block: 'nearest' });
}
return;
}
}
async function updateResults(query = '') {
try {
await searchIndex;
const hasQuery = query.length > 0;
const matches = hasQuery ? searchIndex.search(query) : [];
let id = 0;
let hasResults = hasQuery && matches.length > 0;
siteSearch.classList.toggle('site-search--has-results', hasQuery && hasResults);
siteSearch.classList.toggle('site-search--no-results', hasQuery && !hasResults);
panel.setAttribute('aria-expanded', hasQuery && hasResults ? 'true' : 'false');
results.innerHTML = '';
matches.map(match => {
const page = map[match.ref];
const li = document.createElement('li');
const a = document.createElement('a');
a.href = $docsify.routerMode === 'hash' ? `/#/${page.url}` : `/${page.url}`;
a.innerHTML = `
<h3>${page.title}</h3>
<small>/${page.url}</small>
`;
li.setAttribute('id', `search-result-${id++}`);
li.appendChild(a);
results.appendChild(li);
});
} catch {
// Ignore query errors as the user types
}
}
// Show the search panel slash is pressed outside of a form element
document.addEventListener('keydown', event => {
if (
!isShowing &&
event.key === '/' &&
!event.composedPath().some(el => ['input', 'textarea'].includes(el?.tagName?.toLowerCase()))
) {
event.preventDefault();
show();
}
});
input.addEventListener('sl-input', handleInput);
// Close when a result is selected
results.addEventListener('click', event => {
if (event.target.closest('a')) {
hide();
}
});
});
})();

View File

@@ -1,14 +0,0 @@
(() => {
if (!window.$docsify) {
throw new Error('Docsify must be loaded before installing this plugin.');
}
window.$docsify.plugins.push((hook, vm) => {
hook.mounted(function () {
// Move search below the app name
const appName = document.querySelector('.sidebar .app-name');
const search = document.querySelector('.sidebar .search');
appName.insertAdjacentElement('afterend', search);
});
});
})();

View File

@@ -249,6 +249,11 @@ strong {
max-width: 22rem;
}
.markdown-section .splash-start h1:first-of-type {
font-size: var(--sl-font-size-large);
margin: 0 0 0.5rem 0;
}
@media screen and (max-width: 1040px) {
.splash {
display: block;
@@ -375,12 +380,14 @@ strong {
padding: 2px 4px;
}
kbd,
.markdown-section kbd {
font-family: var(--sl-font-mono);
font-size: 87.5%;
border-radius: var(--sl-border-radius-small);
border: solid 1px rgb(var(--sl-color-neutral-200));
padding: 2px 4px;
box-shadow: var(--sl-shadow-small);
padding: 2px 5px;
}
/* Code blocks */
@@ -706,3 +713,9 @@ body[data-page^='/tokens/'] .table-wrapper td:first-child code {
grid-column-start: span 6;
}
}
.not-found-image {
display: block;
max-width: 460px;
margin: 2rem 0;
}

View File

@@ -2,12 +2,12 @@
<div class="splash-start">
<img class="splash-logo" src="/assets/images/wordmark.svg" alt="Shoelace">
**A forward-thinking library of web components.**
# A forward-thinking library of web components.
- Works with all frameworks 🧩
- Works with CDNs 🚛
- Fully customizable with CSS 🎨
- Includes an official dark theme 🌛
- Includes a dark theme 🌛
- Built with accessibility in mind ♿️
- First-party [React wrappers](/getting-started/usage#react)
- Open source 😸

View File

@@ -34,6 +34,7 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/docsify@4/themes/pure.css" />
<link rel="stylesheet" href="/assets/styles/docs.css" />
<link rel="stylesheet" href="/assets/plugins/code-block/code-block.css" />
<link rel="stylesheet" href="/assets/plugins/search/search.css" />
<link rel="stylesheet" href="/assets/plugins/theme-picker/theme-picker.css" />
<link rel="icon" href="/assets/images/logo.svg" type="image/x-icon" />
<link rel="apple-touch-icon" sizes="180x180" href="/assets/images/touch-icon.png" />
@@ -71,6 +72,7 @@
maxLevel: 3,
subMaxLevel: 2,
name: 'Shoelace',
nameLink: '/',
notFoundPage: '404.md',
pagination: {
previousText: 'Previous',
@@ -79,28 +81,20 @@
crossChapterText: false
},
routerMode: 'history',
search: {
maxAge: 86400000, // Expiration time, the default one day
paths: 'auto',
placeholder: 'Search',
noData: 'No Results',
depth: 3,
namespace: 'shoelace-docs'
},
themeColor: 'rgb(var(--sl-color-primary-500))'
};
</script>
<script src="https://cdn.jsdelivr.net/npm/docsify@4/lib/docsify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify@4/lib/plugins/search.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify@4/lib/plugins/ga.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-copy-code@2"></script>
<script src="https://cdn.jsdelivr.net/npm/docsify-pagination@2/dist/docsify-pagination.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.19.0/components/prism-bash.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/prismjs@1.19.0/components/prism-jsx.min.js"></script>
<script src="/assets/plugins/code-block/code-block.js"></script>
<script src="/assets/plugins/scroll-position/scroll-position.js"></script>
<script src="/assets/plugins/theme-picker/theme-picker.js"></script>
<script src="/assets/plugins/metadata/metadata.js"></script>
<script src="/assets/plugins/sidebar/sidebar.js"></script>
<script src="/assets/plugins/scroll-position/scroll-position.js"></script>
<script src="/assets/plugins/search/lunr.min.js"></script>
<script src="/assets/plugins/search/search.js"></script>
<script src="/assets/plugins/theme-picker/theme-picker.js"></script>
</body>
</html>

View File

@@ -1,6 +1,6 @@
# Community
Shoelace has a budding community of designers and developers that are building amazing things with web components. We'd love for you to become a part of it!
Shoelace has a growing community of designers and developers that are building amazing things with web components. We'd love for you to become a part of it!
Please be respectful of other users and remember that Shoelace is an open source project. We'll try to help when we can, but there's no guarantee we'll be able solve your problem. Please manage your expectations and don't forget to contribute back to the conversation when you can!

13
package-lock.json generated
View File

@@ -37,6 +37,7 @@
"get-port": "^5.1.1",
"globby": "^11.0.4",
"husky": "^4.3.8",
"lunr": "^2.3.9",
"mkdirp": "^0.5.5",
"plop": "^2.7.4",
"prettier": "^2.2.1",
@@ -6429,6 +6430,12 @@
"node": ">=0.10.0"
}
},
"node_modules/lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@@ -16394,6 +16401,12 @@
"integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==",
"dev": true
},
"lunr": {
"version": "2.3.9",
"resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz",
"integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==",
"dev": true
},
"make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",

View File

@@ -67,6 +67,7 @@
"get-port": "^5.1.1",
"globby": "^11.0.4",
"husky": "^4.3.8",
"lunr": "^2.3.9",
"mkdirp": "^0.5.5",
"plop": "^2.7.4",
"prettier": "^2.2.1",

65
scripts/make-search.js Normal file
View File

@@ -0,0 +1,65 @@
import fs from 'fs';
import path from 'path';
import glob from 'globby';
import lunr from 'lunr';
function getHeadings(markdown, maxLevel = 6) {
const headings = [];
const lines = markdown.split('\n');
lines.map(line => {
if (line.startsWith('#')) {
const level = line.match(/^(#+)/)[0].length;
const content = line.replace(/^#+/, '');
if (level <= maxLevel) {
headings.push({ level, content });
}
}
});
return headings;
}
console.log('Generating search index for documentation');
(async () => {
const files = await glob('./docs/**/*.md');
const map = {};
const searchIndex = lunr(function () {
// The search index uses these field names extensively, so shortening them can save some serious bytes. The initial
// index file went from 468 KB => 401 KB.
this.ref('id'); // id
this.field('t', { boost: 10 }); // title
this.field('h', { boost: 5 }); // headings
this.field('c'); // content
files.map((file, index) => {
const relativePath = path.relative('./docs', file);
const url = relativePath.replace(/\.md$/, '');
const filename = path.basename(file);
// Ignore certain directories and files
if (relativePath.startsWith('assets/') || relativePath.startsWith('dist/') || filename === '_sidebar.md') {
return false;
}
const content = fs.readFileSync(file, 'utf8');
const allHeadings = getHeadings(content, 4);
const title =
allHeadings.find(heading => heading.level === 1)?.content ||
path.basename(path.basename(filename), path.extname(filename));
const headings = allHeadings
.filter(heading => heading.level > 1)
.map(heading => heading.content)
.join('\n');
this.add({ id: index, t: title, h: headings, c: content });
map[index] = { title, url };
});
});
fs.writeFileSync('./docs/search.json', JSON.stringify({ searchIndex, map }), 'utf8');
})();

View File

@@ -531,7 +531,7 @@ export default css`
*/
--sl-overlay-background-color: 0 0 0;
--sl-overlay-opacity: 42%;
--sl-overlay-opacity: 67%;
/*
* Panels