hugo-geekdoc/src/js/search.js

201 lines
4.7 KiB
JavaScript

const { groupBy } = require("./groupBy")
const FlexSearch = require("flexsearch")
const Ajv = require("ajv")
document.addEventListener("DOMContentLoaded", function (event) {
const ajv = new Ajv()
const input = document.querySelector("#gdoc-search-input")
const results = document.querySelector("#gdoc-search-results")
const configSchema = {
type: "object",
properties: {
dataFile: {
type: "string"
},
indexConfig: {
type: ["object", "null"]
},
showParent: {
type: "boolean"
}
},
additionalProperties: false
}
getJson("/search/config.min.json", function (searchConfig) {
const configValidate = ajv.compile(configSchema)
const valid = configValidate(searchConfig)
if (!valid)
throw AggregateError(
configValidate.errors.map(
(err) =>
new Error(["Validation error:", err.instancePath, err.keyword, err.message].join(" "))
),
"Schema validation failed"
)
if (input) {
input.addEventListener("focus", () => {
init(input, searchConfig)
})
input.addEventListener("keyup", () => {
search(input, results, searchConfig)
})
}
})
})
function init(input, searchConfig) {
input.removeEventListener("focus", init)
const indexCfgDefaults = {
tokenize: "forward"
}
const indexCfg = searchConfig.indexConfig ? searchConfig.indexConfig : indexCfgDefaults
const dataUrl = searchConfig.dataFile
indexCfg.document = {
key: "id",
index: ["title", "content"],
store: ["title", "href", "parent"]
}
const index = new FlexSearch.Document(indexCfg)
window.geekdocSearchIndex = index
getJson(dataUrl, function (data) {
data.forEach((obj) => {
window.geekdocSearchIndex.add(obj)
})
})
}
function search(input, results, searchConfig) {
const searchCfg = {
enrich: true,
limit: 10
}
while (results.firstChild) {
results.removeChild(results.firstChild)
}
if (!input.value) {
return results.classList.remove("has-hits")
}
let searchHits = flattenHits(window.geekdocSearchIndex.search(input.value, searchCfg))
if (searchHits.length < 1) {
return results.classList.remove("has-hits")
}
results.classList.add("has-hits")
if (searchConfig.showParent === true) {
searchHits = groupBy(searchHits, (hit) => hit.parent)
}
const items = []
if (searchConfig.showParent === true) {
for (const section in searchHits) {
const item = document.createElement("li"),
title = item.appendChild(document.createElement("span")),
subList = item.appendChild(document.createElement("ul"))
title.textContent = section
createLinks(searchHits[section], subList)
items.push(item)
}
} else {
const item = document.createElement("li"),
title = item.appendChild(document.createElement("span")),
subList = item.appendChild(document.createElement("ul"))
title.textContent = "Results"
createLinks(searchHits, subList)
items.push(item)
}
items.forEach((item) => {
results.appendChild(item)
})
}
/**
* Creates links to given fields and either returns them in an array or attaches them to a target element
* @param {Object} fields Page to which the link should point to
* @param {HTMLElement} target Element to which the links should be attatched
* @returns {Array} If target is not specified, returns an array of built links
*/
function createLinks(pages, target) {
const items = []
for (const page of pages) {
const item = document.createElement("li"),
entry = item.appendChild(document.createElement("span")),
a = entry.appendChild(document.createElement("a"))
entry.classList.add("flex")
a.href = page.href
a.textContent = page.title
a.classList.add("gdoc-search__entry")
if (target) {
target.appendChild(item)
continue
}
items.push(item)
}
return items
}
function fetchErrors(response) {
if (!response.ok) {
throw Error("Failed to fetch '" + response.url + "': " + response.statusText)
}
return response
}
function getJson(src, callback) {
fetch(src)
.then(fetchErrors)
.then((response) => response.json())
.then((json) => callback(json))
.catch(function (error) {
if (error instanceof AggregateError) {
console.error(error.message)
error.errors.forEach((element) => {
console.error(element)
})
} else {
console.error(error)
}
})
}
function flattenHits(results) {
const items = []
const map = new Map()
for (const field of results) {
for (const page of field.result) {
if (!map.has(page.doc.href)) {
map.set(page.doc.href, true)
items.push(page.doc)
}
}
}
return items
}