diff options
Diffstat (limited to 'static/js/search.js')
| -rw-r--r-- | static/js/search.js | 366 |
1 files changed, 366 insertions, 0 deletions
diff --git a/static/js/search.js b/static/js/search.js new file mode 100644 index 0000000..cb19e6a --- /dev/null +++ b/static/js/search.js @@ -0,0 +1,366 @@ +window.onload = function() { + if (document.body.contains(document.goSearch)) { + document.goSearch.onsubmit = function() { return goSearchNow() }; + + /* + Source: + - https://github.com/nextapps-de/flexsearch#index-documents-field-search + - https://raw.githack.com/nextapps-de/flexsearch/master/demo/autocomplete.html + - http://elasticlunr.com/ + - https://github.com/getzola/zola/blob/master/docs/static/search.js + - https://github.com/aaranxu/adidoks/blob/main/static/js/search.js + */ + (function(){ + function inputFocus(e) { + + if (e.keyCode === 191//forward slash + && document.activeElement.tagName !== "INPUT" + && document.activeElement.tagName !== "TEXTAREA") { + e.preventDefault(); + searchinput.focus(); + suggestions.classList.remove('d-none'); + } + + if (e.keyCode === 27 ) {//escape + searchinput.blur(); + suggestions.classList.add('d-none'); + closeAllLists(); + } + + const focusableSuggestions= suggestions.querySelectorAll('a'); + if (suggestions.classList.contains('d-none') + || focusableSuggestions.length === 0) { + return; + } + const focusable= [...focusableSuggestions]; + const index = focusable.indexOf(document.activeElement); + + let nextIndex = 0; + + if (e.keyCode === 38) {//up arrow + e.preventDefault(); + nextIndex= index > 0 ? index-1 : 0; + focusableSuggestions[nextIndex].focus(); + } + else if (e.keyCode === 40) {//down arrow + e.preventDefault(); + nextIndex= index+1 < focusable.length ? index+1 : index; + focusableSuggestions[nextIndex].focus(); + } + + } + + var suggestions = document.getElementById("suggestions"); + var searchinput = document.getElementById("searchinput"); + document.addEventListener("keydown", inputFocus); + document.addEventListener("click", function(event) {suggestions.contains(event.target) || suggestions.classList.add("d-none")}); + + var lang = document.documentElement.getAttribute("lang"); + var langOnly = lang.substring(0, 2); + var baseUrl = document.querySelector("meta[name='base']").getAttribute("content"); + if (baseUrl.slice(-1) == "/") { + baseUrl = baseUrl.slice(0, -1); + } + + var index; + searchinput.addEventListener('input', show_results, true); + suggestions.addEventListener('click', accept_suggestion, true); + + + // in page results when press enter or click search icon from search box + function closeSearchNow() { + const main = document.querySelector("main"); + main.innerHTML = window.main + } + + function goSearchNow() { + const main = document.querySelector("main"); + if (!window.main) { + window.main = main.innerHTML + }; + var results = document.getElementById("suggestions");// suggestions div generated by search box + + var ResultsClone = results.cloneNode(true);// make a clone of the results, so that we can alter it + ResultsClone.id = "results";// alter the id of our clone, so that we can apply different css style + + var headerDiv = document.createElement("div");// create a div element + + var headerContent = '<form name="closeSearch"><h2><button type="submit" title="Close Search"><i class="svgs x"></i></button> Results For: '.concat(document.getElementById("searchinput").value, "</h2></form>");// header to use at top of results page + + headerDiv.innerHTML = headerContent;// document element div (headerDiv), set the inner contents to our header html (headerContent) + + ResultsClone.insertBefore(headerDiv, ResultsClone.firstChild);//insert our header div at the top of the page + + main.innerHTML = ResultsClone.outerHTML;//display ResultsClone.outerHTML as the page + results.innerHTML = "";// clear the suggestions div popup + document.getElementById("searchinput").value = "";// clear the search input box + document.body.contains(document.closeSearch) && (document.closeSearch.onsubmit = function() { closeSearchNow() }) + return false + } + + /* Close search suggestion popup list */ + function closeAllLists(elmnt) { + var suggestions = document.getElementById("suggestions"); + while (suggestions.firstChild) { + suggestions.removeChild(suggestions.firstChild); + } + } + + + + async function show_results() { + var initIndex = async function () { + if (index === undefined) { + index = fetch(baseUrl + '/search_index.' + langOnly + '.json') + .then( + async function(response) { + return await elasticlunr.Index.load(await response.json()); + } + ); + } + let res = await index; + return res; + } + var value = this.value.trim(); + var options = { + bool: "OR", + fields: { + title: {boost: 2}, + body: {boost: 1}, + } + }; + //var results = index.search(value, options); + var results = (await initIndex()).search(value, options); + + var entry, childs = suggestions.childNodes; + var i = 0, len = results.length; + var items = value.split(/\s+/); + suggestions.classList.remove('d-none'); + + results.forEach(function(page) { + if (page.doc.body !== '') { + entry = document.createElement('div'); + + entry.innerHTML = '<a href><span></span><span></span></a>'; + + a = entry.querySelector('a'), + t = entry.querySelector('span:first-child'), + d = entry.querySelector('span:nth-child(2)'); + a.href = page.ref; + t.textContent = page.doc.title; + d.innerHTML = makeTeaser(page.doc.body, items); + + suggestions.appendChild(entry); + } + }); + + while(childs.length > len){ + suggestions.removeChild(childs[i]) + } + + } + + function accept_suggestion(){ + + while(suggestions.lastChild){ + + suggestions.removeChild(suggestions.lastChild); + } + + return false; + } + // Get the string bytes from binary + function getByteByBinary(binaryCode) { + // Binary system, starts with `0b` in ES6 + // Octal number system, starts with `0` in ES5 and starts with `0o` in ES6 + // Hexadecimal, starts with `0x` in both ES5 and ES6 + var byteLengthDatas = [0, 1, 2, 3, 4]; + var len = byteLengthDatas[Math.ceil(binaryCode.length / 8)]; + return len; + } + + // Get the string bytes from hexadecimal + function getByteByHex(hexCode) { + return getByteByBinary(parseInt(hexCode, 16).toString(2)); + } + // Get substring by bytes + // If using JavaScript inline substring method, it will return error codes + // Source: https://www.52pojie.cn/thread-1059814-1-1.html + function substringByByte(str, maxLength) { + var result = ""; + var flag = false; + var len = 0; + var length = 0; + var length2 = 0; + for (var i = 0; i < str.length; i++) { + var code = str.codePointAt(i).toString(16); + if (code.length > 4) { + i++; + if ((i + 1) < str.length) { + flag = str.codePointAt(i + 1).toString(16) == "200d"; + } + } + if (flag) { + len += getByteByHex(code); + if (i == str.length - 1) { + length += len; + if (length <= maxLength) { + result += str.substr(length2, i - length2 + 1); + } else { + break + } + } + } else { + if (len != 0) { + length += len; + length += getByteByHex(code); + if (length <= maxLength) { + result += str.substr(length2, i - length2 + 1); + length2 = i + 1; + } else { + break + } + len = 0; + continue; + } + length += getByteByHex(code); + if (length <= maxLength) { + if (code.length <= 4) { + result += str[i] + } else { + result += str[i - 1] + str[i] + } + length2 = i + 1; + } else { + break + } + } + } + return result; + } + /* Taken from mdbook + // The strategy is as follows: + // First, assign a value to each word in the document: + // Words that correspond to search terms (stemmer aware): 40 + // Normal words: 2 + // First word in a sentence: 8 + // Then use a sliding window with a constant number of words and count the + // sum of the values of the words within the window. Then use the window that got the + // maximum sum. If there are multiple maximas, then get the last one. + // Enclose the terms in <b>. + */ + function makeTeaser(body, terms) { + var TERM_WEIGHT = 40; + var NORMAL_WORD_WEIGHT = 2; + var FIRST_WORD_WEIGHT = 8; + var TEASER_MAX_WORDS = 30; + + var stemmedTerms = terms.map(function (w) { + return elasticlunr.stemmer(w.toLowerCase()); + }); + var termFound = false; + var index = 0; + var weighted = []; // contains elements of ["word", weight, index_in_document] + + // split in sentences, then words + var sentences = body.toLowerCase().split(". "); + for (var i in sentences) { + var words = sentences[i].split(/[\s\n]/); + var value = FIRST_WORD_WEIGHT; + for (var j in words) { + + var word = words[j]; + + if (word.length > 0) { + for (var k in stemmedTerms) { + if (elasticlunr.stemmer(word).startsWith(stemmedTerms[k])) { + value = TERM_WEIGHT; + termFound = true; + } + } + weighted.push([word, value, index]); + value = NORMAL_WORD_WEIGHT; + } + + index += word.length; + index += 1; // ' ' or '.' if last word in sentence + } + + index += 1; // because we split at a two-char boundary '. ' + } + + if (weighted.length === 0) { + if (body.length !== undefined && body.length > TEASER_MAX_WORDS * 10) { + return body.substring(0, TEASER_MAX_WORDS * 10) + '...'; + } else { + return body; + } + } + + var windowWeights = []; + var windowSize = Math.min(weighted.length, TEASER_MAX_WORDS); + // We add a window with all the weights first + var curSum = 0; + for (var i = 0; i < windowSize; i++) { + curSum += weighted[i][1]; + } + windowWeights.push(curSum); + + for (var i = 0; i < weighted.length - windowSize; i++) { + curSum -= weighted[i][1]; + curSum += weighted[i + windowSize][1]; + windowWeights.push(curSum); + } + + // If we didn't find the term, just pick the first window + var maxSumIndex = 0; + if (termFound) { + var maxFound = 0; + // backwards + for (var i = windowWeights.length - 1; i >= 0; i--) { + if (windowWeights[i] > maxFound) { + maxFound = windowWeights[i]; + maxSumIndex = i; + } + } + } + + var teaser = []; + var startIndex = weighted[maxSumIndex][2]; + for (var i = maxSumIndex; i < maxSumIndex + windowSize; i++) { + var word = weighted[i]; + if (startIndex < word[2]) { + // missing text from index to start of `word` + teaser.push(body.substring(startIndex, word[2])); + startIndex = word[2]; + } + + // add <em/> around search terms + if (word[1] === TERM_WEIGHT) { + teaser.push("<b>"); + } + + startIndex = word[2] + word[0].length; + // Check the string is ascii characters or not + var re = /^[\x00-\xff]+$/ + if (word[1] !== TERM_WEIGHT && word[0].length >= 12 && !re.test(word[0])) { + // If the string's length is too long, it maybe a Chinese/Japance/Korean article + // if using substring method directly, it may occur error codes on emoji chars + var strBefor = body.substring(word[2], startIndex); + var strAfter = substringByByte(strBefor, 12); + teaser.push(strAfter); + } else { + teaser.push(body.substring(word[2], startIndex)); + } + + if (word[1] === TERM_WEIGHT) { + teaser.push("</b>"); + } + } + teaser.push("…"); + return teaser.join(""); + } + document.goSearch.onsubmit = function() { return goSearchNow() }; + }()); + } +}; |
