breezewiki/static/search-suggestions.js

100 lines
3.1 KiB
JavaScript

import {h, htm, render, signal, computed, effect} from "./preact.js"
const html = htm.bind(h)
const classNames = classArr => classArr.filter(el => el).join(" ")
const eForm = document.getElementById("bw-pr-search-form")
const eInput = document.getElementById("bw-pr-search-input")
const eSuggestions = document.getElementById("bw-pr-search-suggestions")
const hitsPromise = new Map()
const hitsDone = new Set()
const query = signal("")
const focus = signal(false)
const st = signal("ready")
const suggestions = signal([])
// processing functions
function fetchSuggestions(query, setSuggestions) {
if (query === "") query = "\0"
if (hitsPromise.has(query)) return hitsPromise.get(query)
const url = new URL(`https://${BWData.wikiname}.fandom.com/api.php`)
url.searchParams.set("action", "opensearch")
url.searchParams.set("format", "json")
url.searchParams.set("namespace", "0") // wiki namespace, 0 is default
url.searchParams.set("origin", "*") // mediawiki api cors
url.searchParams.set("search", query)
const sendUrl = BWData.strict_proxy
? "/proxy?" + new URLSearchParams({dest: url})
: url
const promise = fetch(sendUrl).then(res => res.json()).then(root => {
hitsDone.add(query)
return Array(root[1].length).fill().map((_, i) => ({
title: root[1][i],
url: root[3][i]
}))
})
hitsPromise.set(query, promise)
return promise
}
function acceptSuggestion(hit) {
st.value = "accepted"
query.value = hit.title
const dest = new URL(hit.url).pathname.match("/wiki/.*")
location = `/${BWData.wikiname}${dest}`
}
// suggestion list view
function Suggestion(hit) {
return html`<li class="bw-ss__item"><button type="button" class="bw-ss__button" onClick=${() => acceptSuggestion(hit)}>${hit.title}</button></li>`
}
function SuggestionList() {
return html`
<ul class=${classNames(["bw-ss__list", focus.value && "bw-ss__list--focus", `bw-ss__list--${st.value}`])}>
${suggestions.value.map(hit => html`<${Suggestion} ...${hit} />`)}
</ul>`
}
render(html`<${SuggestionList} />`, eSuggestions)
// input view
effect(() => {
query.value // dependency should always be tracked
if (st.peek() === "accepted") return // lock results from changing during navigation
st.value = "loading"
fetchSuggestions(query.value).then(res => {
suggestions.value = res
if (hitsDone.size === hitsPromise.size) {
st.value = "ready"
}
})
})
window.addEventListener("pageshow", () => {
st.value = "ready" // unlock results from changing after returning to page
})
function SuggestionInput() {
return html`
<input type="text" name="q" id="bw-search-input" autocomplete="off" onInput=${e => query.value = e.target.value} value=${query.value} class=${classNames(["bw-ss__input", `bw-ss__input--${st.value}`])} />`
}
render(html`<${SuggestionInput} />`, eInput)
// form focus
eForm.addEventListener("focusin", () => focus.value = true)
eForm.addEventListener("focusout", event => {
if (eForm.contains(event.relatedTarget)) {
// event fired when changing from one form element to the other
focus.value = true
} else {
// event fired when moving out of the form element
focus.value = false
}
})