2022-01-06 12:58:10 +00:00
|
|
|
const { groupBy } = require("./groupBy")
|
2022-01-08 16:44:53 +00:00
|
|
|
const { FlexSearch } = require("flexsearch/dist/flexsearch.compact")
|
2022-01-08 20:29:28 +00:00
|
|
|
const { Validator } = require("@cfworker/json-schema")
|
2022-01-06 12:58:10 +00:00
|
|
|
|
|
|
|
document.addEventListener("DOMContentLoaded", function (event) {
|
|
|
|
const input = document.querySelector("#gdoc-search-input")
|
|
|
|
const results = document.querySelector("#gdoc-search-results")
|
2022-01-16 15:03:31 +00:00
|
|
|
const basePath = urlPath(input ? input.dataset.siteBaseUrl : "")
|
2022-01-23 12:21:44 +00:00
|
|
|
const lang = input ? input.dataset.siteLang : ""
|
2022-01-06 12:58:10 +00:00
|
|
|
|
|
|
|
const configSchema = {
|
|
|
|
type: "object",
|
|
|
|
properties: {
|
|
|
|
dataFile: {
|
|
|
|
type: "string"
|
|
|
|
},
|
|
|
|
indexConfig: {
|
|
|
|
type: ["object", "null"]
|
|
|
|
},
|
|
|
|
showParent: {
|
|
|
|
type: "boolean"
|
|
|
|
}
|
|
|
|
},
|
|
|
|
additionalProperties: false
|
|
|
|
}
|
2022-01-08 20:29:28 +00:00
|
|
|
const validator = new Validator(configSchema)
|
2022-01-06 12:58:10 +00:00
|
|
|
|
2022-01-16 15:03:31 +00:00
|
|
|
if (!input) return
|
|
|
|
|
2022-01-23 12:21:44 +00:00
|
|
|
getJson(combineURLs(basePath, "/search/" + lang + ".config.min.json"), function (searchConfig) {
|
2022-01-08 20:29:28 +00:00
|
|
|
const validationResult = validator.validate(searchConfig)
|
2022-01-06 12:58:10 +00:00
|
|
|
|
2022-01-08 20:29:28 +00:00
|
|
|
if (!validationResult.valid)
|
2022-01-06 12:58:10 +00:00
|
|
|
throw AggregateError(
|
2022-01-08 20:29:28 +00:00
|
|
|
validationResult.errors.map((err) => new Error("Validation error: " + err.error)),
|
2022-01-06 12:58:10 +00:00
|
|
|
"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
|
|
|
|
}
|
2022-01-16 15:03:31 +00:00
|
|
|
|
|
|
|
function urlPath(rawURL) {
|
|
|
|
var parser = document.createElement("a")
|
|
|
|
parser.href = rawURL
|
|
|
|
|
|
|
|
return parser.pathname
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Part of [axios](https://github.com/axios/axios/blob/master/lib/helpers/combineURLs.js).
|
|
|
|
* Creates a new URL by combining the specified URLs
|
|
|
|
*
|
|
|
|
* @param {string} baseURL The base URL
|
|
|
|
* @param {string} relativeURL The relative URL
|
|
|
|
* @returns {string} The combined URL
|
|
|
|
*/
|
|
|
|
function combineURLs(baseURL, relativeURL) {
|
|
|
|
return relativeURL
|
|
|
|
? baseURL.replace(/\/+$/, "") + "/" + relativeURL.replace(/^\/+/, "")
|
|
|
|
: baseURL
|
|
|
|
}
|