diff --git a/assets/ts/search.tsx b/assets/ts/search.tsx index 8be4b7d..9539caa 100644 --- a/assets/ts/search.tsx +++ b/assets/ts/search.tsx @@ -8,17 +8,6 @@ interface pageData { matchCount: number } -const searchForm = document.querySelector('.search-form') as HTMLFormElement; -const searchInput = searchForm.querySelector('input') as HTMLInputElement; -const searchResultList = document.querySelector('.search-result--list') as HTMLDivElement; -const searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement; - -let data: pageData[]; - -function escapeRegExp(string) { - return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); -} - /** * Escape HTML tags as HTML entities * Edited from: @@ -40,187 +29,222 @@ function replaceHTMLEnt(str) { return str.replace(/[&<>"]/g, replaceTag); } -async function getData() { - if (!data) { - /// Not fetched yet - const jsonURL = searchForm.dataset.json; - data = await fetch(jsonURL).then(res => res.json()); - } - - return data; +function escapeRegExp(string) { + return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); } -function updateQueryString(keywords: string, replaceState = false) { - const pageURL = new URL(window.location.toString()); +class Search { + private data: pageData[]; + private form: HTMLFormElement; + private input: HTMLInputElement; + private list: HTMLDivElement; + private resultTitle: HTMLHeadElement; - if (keywords === '') { - pageURL.searchParams.delete('keyword') - } - else { - pageURL.searchParams.set('keyword', keywords); + constructor({ form, input, list, resultTitle }) { + this.form = form; + this.input = input; + this.list = list; + this.resultTitle = resultTitle; + + this.handleQueryString(); + this.bindQueryStringChange(); + this.bindSearchForm(); } - if (replaceState) { - window.history.replaceState('', '', pageURL.toString()); + private async searchKeywords(keywords: string[]) { + const rawData = await this.getData(); + let results: pageData[] = []; + + /// Sort keywords by their length + keywords.sort((a, b) => { + return b.length - a.length + }); + + for (const item of rawData) { + let result = { + ...item, + preview: '', + matchCount: 0 + } + + let matched = false; + + for (const keyword of keywords) { + if (keyword === '') continue; + + const regex = new RegExp(escapeRegExp(replaceHTMLEnt(keyword)), 'gi'); + + const contentMatch = regex.exec(result.content); + regex.lastIndex = 0; /// Reset regex + + const titleMatch = regex.exec(result.title); + regex.lastIndex = 0; /// Reset regex + + if (titleMatch) { + result.title = result.title.replace(regex, Search.marker); + } + + if (titleMatch || contentMatch) { + matched = true; + ++result.matchCount; + + let start = 0, + end = 100; + + if (contentMatch) { + start = contentMatch.index - 20; + end = contentMatch.index + 80 + + if (start < 0) start = 0; + } + + if (result.preview.indexOf(keyword) !== -1) { + result.preview = result.preview.replace(regex, Search.marker); + } + else { + if (start !== 0) result.preview += `[...] `; + result.preview += `${result.content.slice(start, end).replace(regex, Search.marker)} `; + } + } + } + + if (matched) { + result.preview += '[...]'; + results.push(result); + } + } + + /** Result with more matches appears first */ + return results.sort((a, b) => { + return b.matchCount - a.matchCount; + }); } - else { - window.history.pushState('', '', pageURL.toString()); + + public static marker(match) { + return '' + match + ''; } -} -function bindQueryStringChange() { - window.addEventListener('popstate', (e) => { - handleQueryString() - }) -} + private async doSearch(keywords: string[]) { + const startTime = performance.now(); -function handleQueryString() { - const pageURL = new URL(window.location.toString()); - const keywords = pageURL.searchParams.get('keyword'); - searchInput.value = keywords; + const results = await this.searchKeywords(keywords); + this.clear(); - if (keywords) { - doSearch(keywords.split(' ')); + for (const item of results) { + this.list.append(Search.render(item)); + } + + const endTime = performance.now(); + + this.resultTitle.innerText = `${results.length} pages (${((endTime - startTime) / 1000).toPrecision(1)} seconds)`; } - else { - clear() + + public async getData() { + if (!this.data) { + /// Not fetched yet + const jsonURL = this.form.dataset.json; + this.data = await fetch(jsonURL).then(res => res.json()); + } + + return this.data; } -} -function bindSearchForm() { - let lastSearch = ''; + private bindSearchForm() { + let lastSearch = ''; - const eventHandler = (e) => { - e.preventDefault(); - const keywords = searchInput.value; + const eventHandler = (e) => { + e.preventDefault(); + const keywords = this.input.value; - updateQueryString(keywords, true); + Search.updateQueryString(keywords, true); + + if (keywords === '') { + return this.clear(); + } + + if (lastSearch === keywords) return; + lastSearch = keywords; + + this.doSearch(keywords.split(' ')); + } + + this.input.addEventListener('input', eventHandler); + this.input.addEventListener('compositionend', eventHandler); + } + + private clear() { + this.list.innerHTML = ''; + this.resultTitle.innerText = ''; + } + + private bindQueryStringChange() { + window.addEventListener('popstate', (e) => { + this.handleQueryString() + }) + } + + private handleQueryString() { + const pageURL = new URL(window.location.toString()); + const keywords = pageURL.searchParams.get('keyword'); + this.input.value = keywords; + + if (keywords) { + this.doSearch(keywords.split(' ')); + } + else { + this.clear() + } + } + + private static updateQueryString(keywords: string, replaceState = false) { + const pageURL = new URL(window.location.toString()); if (keywords === '') { - return clear(); + pageURL.searchParams.delete('keyword') + } + else { + pageURL.searchParams.set('keyword', keywords); } - if (lastSearch === keywords) return; - lastSearch = keywords; - - doSearch(keywords.split(' ')); - } - - searchInput.addEventListener('input', eventHandler); - searchInput.addEventListener('compositionend', eventHandler); -} - -function clear() { - searchResultList.innerHTML = ''; - searchResultTitle.innerText = ''; -} - -async function doSearch(keywords: string[]) { - const startTime = performance.now(); - - const results = await searchKeywords(keywords); - clear(); - - for (const item of results) { - searchResultList.append(render(item)); - } - - const endTime = performance.now(); - - searchResultTitle.innerText = `${results.length} pages (${((endTime - startTime) / 1000).toPrecision(1)} seconds)`; -} - -function marker(match) { - return '' + match + ''; -} - -async function searchKeywords(keywords: string[]) { - const rawData = await getData(); - let results: pageData[] = []; - - /// Sort keywords by their length - keywords.sort((a, b) => { - return b.length - a.length - }); - - for (const item of rawData) { - let result = { - ...item, - preview: '', - matchCount: 0 + if (replaceState) { + window.history.replaceState('', '', pageURL.toString()); } - - let matched = false; - - for (const keyword of keywords) { - if (keyword === '') continue; - - const regex = new RegExp(escapeRegExp(replaceHTMLEnt(keyword)), 'gi'); - - const contentMatch = regex.exec(result.content); - regex.lastIndex = 0; /// Reset regex - - const titleMatch = regex.exec(result.title); - regex.lastIndex = 0; /// Reset regex - - if (titleMatch) { - result.title = result.title.replace(regex, marker); - } - - if (titleMatch || contentMatch) { - matched = true; - ++result.matchCount; - - let start = 0, - end = 100; - - if (contentMatch) { - start = contentMatch.index - 20; - end = contentMatch.index + 80 - - if (start < 0) start = 0; - } - - if (result.preview.indexOf(keyword) !== -1) { - result.preview = result.preview.replace(regex, marker); - } - else { - if (start !== 0) result.preview += `[...] `; - result.preview += `${result.content.slice(start, end).replace(regex, marker)} `; - } - } - } - - if (matched) { - result.preview += '[...]'; - results.push(result); + else { + window.history.pushState('', '', pageURL.toString()); } } - /** Result with more matches appears first */ - return results.sort((a, b) => { - return b.matchCount - a.matchCount; - }); -} - -const render = (item: pageData) => { - return
- -
-

- -
- {item.image && -
- + public static render(item: pageData) { + return ; + {item.image && +
+ +
+ } + +
; + } } -window.addEventListener('DOMContentLoaded', () => { - handleQueryString(); - bindQueryStringChange(); - bindSearchForm(); -}) \ No newline at end of file +window.addEventListener('load', () => { + setTimeout(function () { + const searchForm = document.querySelector('.search-form') as HTMLFormElement, + searchInput = searchForm.querySelector('input') as HTMLInputElement, + searchResultList = document.querySelector('.search-result--list') as HTMLDivElement, + searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement; + + new Search({ + form: searchForm, + input: searchInput, + list: searchResultList, + resultTitle: searchResultTitle + }); + }, 0); +}) + +export default Search; \ No newline at end of file