Last active
December 18, 2022 06:12
-
-
Save basic-calculus/bf66906872a3d870fcff038164802816 to your computer and use it in GitHub Desktop.
Side by side with org mode. Something like this might work? It would be nice if you could use a more typical markup language like markdown though
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [[file:search.mjs::history.scrollRestoration]] | |
| FIXME unsure about scroll restoration. | |
| [[file:search.mjs::function clickRequest(]] | |
| Filter to non modifier, non chorded mouse clicks. | |
| [[file:search.mjs::function submitRequest(]] | |
| work around an incorrect action string for .formAction here. | |
| [[file:search.mjs::function target(]] | |
| FIXME what to place for the fallback/default target here? | |
| [[file:search.mjs::targeting = true]] | |
| This is a little ugly but replace can trigger popstate (at least in | |
| Chrome) and lead to a stack overflow. | |
| [[file:search.mjs::h1.focus()]] | |
| Timeout then blur? | |
| [[file:search.mjs::function search()]] | |
| FIXME set options for tags/category in parseParams. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| const pfimp = import('./pagefind/pagefind.js'); | |
| history.scrollRestoration = 'manual'; | |
| const { origin, pathname, searchParams } = new URL(location); | |
| function searchlink(p, x) { | |
| const params = new URLSearchParams({[p]: x}); | |
| return `${pathname}?${params}`; | |
| } | |
| async function fetchjson(url) { | |
| return await ((await fetch(url)).json()); | |
| } | |
| customElements.define("search-h1", class extends HTMLHeadingElement { | |
| static observedAttributes = ['data-query']; | |
| #query; | |
| #shadow; | |
| constructor() { | |
| super(); | |
| this.#shadow = this.attachShadow({ | |
| mode: 'closed', | |
| delegatesFocus: false | |
| }); | |
| } | |
| connectedCallback() { | |
| if (!this.isConnected) { | |
| return; | |
| } | |
| if (this.#query) { | |
| return; | |
| } | |
| const template = this.ownerDocument.getElementById('search-h1').content; | |
| const copy = this.ownerDocument.importNode(template, true); | |
| this.#shadow.appendChild(copy); | |
| this.#query = this.#shadow.getElementById('query'); | |
| } | |
| attributeChangedCallback(n, o, x) { | |
| this.#query.textContent = x ? `${x} - ` : ''; | |
| } | |
| }, { 'extends': 'h1' }); | |
| customElements.define("search-result", class extends HTMLElement { | |
| #init = false; | |
| #shadow; | |
| constructor() { | |
| super(); | |
| this.#shadow = this.attachShadow({ mode: 'closed' }); | |
| } | |
| connectedCallback() { | |
| if (!this.isConnected) { | |
| return; | |
| } | |
| if (this.#init) { | |
| return; | |
| } | |
| const template = this.ownerDocument.getElementById('search-result').content; | |
| const copy = this.ownerDocument.importNode(template, true); | |
| this.#shadow.appendChild(copy); | |
| this.#init = true; | |
| } | |
| }); | |
| function renderPost(post, result) { | |
| const { title, url, date, tags, categories, excerpt } = post; | |
| const exc = document.createElement("div"); | |
| exc.innerHTML = excerpt; | |
| result.append( | |
| Object.assign( | |
| document.createElement('a'), | |
| { slot: 'title', | |
| href: url, | |
| textContent: title }), | |
| Object.assign( | |
| document.createElement('time'), | |
| { slot: 'date', | |
| textContent: date }), | |
| ...categories.map( | |
| category => | |
| Object.assign(document.createElement('a'), | |
| { | |
| slot: 'category', | |
| href: searchlink('category', category), | |
| textContent: category | |
| })), | |
| ...tags.map(tag => | |
| Object.assign(document.createElement('a'), | |
| { | |
| slot: 'tag', | |
| href: searchlink('tag', tag), | |
| textContent: `#${tag}` | |
| })), | |
| exc); | |
| } | |
| function fromPagefind(post) { | |
| const { url, | |
| excerpt, | |
| meta: { title, date }, | |
| filters: { tag, category } } = post; | |
| return { | |
| url: url, | |
| title: title, | |
| date: date, | |
| categories: category ?? [], | |
| tags: tag ?? [], | |
| excerpt | |
| }; | |
| } | |
| async function findPosts(query, options) { | |
| if ('' == query) { | |
| query = null; | |
| } | |
| const { categories, tags } = options; | |
| const filters = {}; | |
| if (categories) { | |
| filters.category = Array.from(categories); | |
| } | |
| if (tags) { | |
| filters.tag = Array.from(tags); | |
| } | |
| return (await (await pfimp).search(query, { filters: filters })).results; | |
| } | |
| async function renderPosts(search, results) { | |
| await Promise.all( | |
| search.map((r, ix) => | |
| r.data() | |
| .then(post => { | |
| renderPost(fromPagefind(post), results[ix]); | |
| }))); | |
| } | |
| function anchorRequest(anchor) { | |
| const { href, nodeName, origin: tagOrigin } = anchor; | |
| if (nodeName !== 'A') { | |
| return; | |
| } | |
| if (tagOrigin !== origin) { | |
| return; | |
| } | |
| const good = { | |
| username: '', | |
| target: '', | |
| password: '', | |
| download: '' | |
| }; | |
| for (const [k, v] of Object.entries(good)) { | |
| if (anchor[k] !== v) { | |
| return; | |
| } | |
| } | |
| if (!href) { | |
| return; | |
| } | |
| return new Request(href); | |
| } | |
| function modifierKey(event) { | |
| const { ctrlKey, altKey, shiftKey, metaKey } = event; | |
| return ctrlKey || altKey || shiftKey || metaKey; | |
| } | |
| function clickRequest(event) { | |
| const { button, buttons, target } = event; | |
| if (modifierKey(event)) { | |
| return; | |
| } | |
| if (button != 0 || buttons != 0) { | |
| return; | |
| } | |
| return anchorRequest(target); | |
| } | |
| function keydownRequest(event) { | |
| const { isComposing, key, target } = event; | |
| if (isComposing) { | |
| return; | |
| } | |
| if (modifierKey(event)) { | |
| return; | |
| } | |
| if (key !== "Enter") { | |
| return; | |
| } | |
| return anchorRequest(target); | |
| } | |
| function submitRequest(event) { | |
| const { submitter, target: form } = event; | |
| const action = submitter?.getAttribute('formaction') ?? form.action; | |
| const method = submitter?.getAttribute('method') ?? form.method; | |
| let url = new URL(action, origin); | |
| const { origin: urlOrigin, pathname, searchParams } = url; | |
| if (urlOrigin != origin) { | |
| return; | |
| } | |
| const formdata = new FormData(form); | |
| const options = { method: method }; | |
| if (method === 'get') { | |
| const params = new URLSearchParams(formdata); | |
| for (const [key, value] of searchParams) { | |
| params.append(key, value); | |
| } | |
| url = new URL(urlOrigin + pathname + "?" + params); | |
| } else { | |
| options.body = formdata; | |
| } | |
| return new Request(url, options); | |
| } | |
| function parseParams(params) { | |
| return { | |
| query: params.get('s') ?? '', | |
| category: new Set(params.getAll('category')), | |
| tag: new Set(params.getAll('tag')) | |
| }; | |
| } | |
| function route(req) { | |
| const { method, url } = req; | |
| const { searchParams, pathname } = new URL(url); | |
| switch (pathname) { | |
| case '/search/': | |
| switch (method) { | |
| case 'GET': | |
| return (async () => await search(searchParams)); | |
| } | |
| break; | |
| } | |
| } | |
| let targeting = false; | |
| function target(url) { | |
| const fallback = '#'; | |
| let { hash } = new URL(url); | |
| if (hash == '') { | |
| hash = fallback; | |
| } | |
| targeting = true; | |
| location.replace(hash); | |
| targeting = false; | |
| const h1 = document.getElementsByTagName('h1')[0]; | |
| if (h1) { | |
| h1.tabIndex = -1; | |
| h1.focus(); | |
| } | |
| } | |
| async function keydown(event) { | |
| const r = keydownRequest(event); | |
| if (!r) { | |
| return; | |
| } | |
| const action = route(r); | |
| if (!action) { | |
| return; | |
| } | |
| event.preventDefault(); | |
| history.pushState(null, '', r.url); | |
| await action(); | |
| target(r.url); | |
| } | |
| async function click(event) { | |
| const r = clickRequest(event); | |
| if (!r) { | |
| return; | |
| } | |
| const action = route(r); | |
| if (!action) { | |
| return; | |
| } | |
| event.preventDefault(); | |
| history.pushState(null, '', r.url); | |
| await action(); | |
| target(r.url); | |
| } | |
| async function submit(event) { | |
| const r = submitRequest(event); | |
| if (!r) { | |
| return; | |
| } | |
| const action = route(r); | |
| if (!action) { | |
| return; | |
| } | |
| event.preventDefault(); | |
| history.pushState(null, '', r.url); | |
| await action(); | |
| target(r.url); | |
| } | |
| async function popstate(event) { | |
| if (targeting) { | |
| return; | |
| } | |
| const r = new Request(location); | |
| const action = route(r); | |
| if (!action) { | |
| return; | |
| } | |
| await action(); | |
| target(r.url); | |
| } | |
| const doctitle = document.title; | |
| async function search(searchParams) { | |
| const { query, category, tag } = parseParams(searchParams); | |
| const postsPs = findPosts(query, { | |
| tags: tag, | |
| categories: category | |
| }); | |
| switch (document.readyState) { | |
| case 'interactive': | |
| case 'complete': | |
| break; | |
| default: | |
| await new Promise(r => { | |
| window.addEventListener('DOMContentLoaded', r); | |
| }); | |
| break; | |
| } | |
| const h1 = document.getElementById('title'); | |
| const input = document.getElementById('search-input'); | |
| const output = document.getElementById('search-output'); | |
| const list = document.getElementById('search-list'); | |
| const categoryEl = document.getElementById('category'); | |
| const tagEl = document.getElementById('tag'); | |
| input && (input.value = query); | |
| h1 && (h1.dataset.query = query); | |
| document.title = `${query} — ${doctitle}`; | |
| if (categoryEl) { | |
| for (const option of categoryEl.options) { | |
| option.selected = category.has(option.value); | |
| } | |
| } | |
| if (tagEl) { | |
| for (const option of tagEl.options) { | |
| option.selected = tag.has(option.value); | |
| } | |
| } | |
| if (output) { | |
| const posts = await postsPs; | |
| const lis = posts.map(() => document.createElement('li')); | |
| output.ariaHidden = "true"; | |
| list.replaceChildren(...lis); | |
| const results = lis.map(li => { | |
| const result = document.createElement('search-result'); | |
| li.replaceChildren(result); | |
| return result; | |
| }); | |
| await renderPosts(posts, results); | |
| output.ariaHidden = "false"; | |
| } | |
| } | |
| document.addEventListener('click', click); | |
| document.addEventListener('keydown', keydown); | |
| document.addEventListener('submit', submit); | |
| window.addEventListener('popstate', popstate); | |
| search(searchParams); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment