diff options
author | Jimmy Cai <jimmehcai@gmail.com> | 2020-11-06 13:33:51 +0300 |
---|---|---|
committer | Jimmy Cai <jimmehcai@gmail.com> | 2020-11-06 13:33:51 +0300 |
commit | f5d45458fd55a4fdbff846f132d039518b07d3a3 (patch) | |
tree | 38befe2d1eb89c325556e5b4b37ab5796b71d7e4 | |
parent | b97e86a7a763ebac35a3390f144da955626ba4db (diff) |
refactor(search): create Search class
-rw-r--r-- | assets/ts/search.tsx | 324 |
1 files changed, 174 insertions, 150 deletions
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()); - - if (keywords === '') { - pageURL.searchParams.delete('keyword') - } - else { - pageURL.searchParams.set('keyword', keywords); +class Search { + private data: pageData[]; + private form: HTMLFormElement; + private input: HTMLInputElement; + private list: HTMLDivElement; + private resultTitle: HTMLHeadElement; + + 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()); - } - else { - window.history.pushState('', '', pageURL.toString()); - } -} + private async searchKeywords(keywords: string[]) { + const rawData = await this.getData(); + let results: pageData[] = []; -function bindQueryStringChange() { - window.addEventListener('popstate', (e) => { - handleQueryString() - }) -} + /// Sort keywords by their length + keywords.sort((a, b) => { + return b.length - a.length + }); -function handleQueryString() { - const pageURL = new URL(window.location.toString()); - const keywords = pageURL.searchParams.get('keyword'); - searchInput.value = keywords; + for (const item of rawData) { + let result = { + ...item, + preview: '', + matchCount: 0 + } - if (keywords) { - doSearch(keywords.split(' ')); - } - else { - clear() - } -} + let matched = false; -function bindSearchForm() { - let lastSearch = ''; + for (const keyword of keywords) { + if (keyword === '') continue; - const eventHandler = (e) => { - e.preventDefault(); - const keywords = searchInput.value; + const regex = new RegExp(escapeRegExp(replaceHTMLEnt(keyword)), 'gi'); - updateQueryString(keywords, true); + const contentMatch = regex.exec(result.content); + regex.lastIndex = 0; /// Reset regex - if (keywords === '') { - return clear(); - } + const titleMatch = regex.exec(result.title); + regex.lastIndex = 0; /// Reset regex - if (lastSearch === keywords) return; - lastSearch = keywords; + if (titleMatch) { + result.title = result.title.replace(regex, Search.marker); + } - doSearch(keywords.split(' ')); - } + if (titleMatch || contentMatch) { + matched = true; + ++result.matchCount; - searchInput.addEventListener('input', eventHandler); - searchInput.addEventListener('compositionend', eventHandler); -} + let start = 0, + end = 100; -function clear() { - searchResultList.innerHTML = ''; - searchResultTitle.innerText = ''; -} + if (contentMatch) { + start = contentMatch.index - 20; + end = contentMatch.index + 80 -async function doSearch(keywords: string[]) { - const startTime = performance.now(); + 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); + } + } - const results = await searchKeywords(keywords); - clear(); + /** Result with more matches appears first */ + return results.sort((a, b) => { + return b.matchCount - a.matchCount; + }); + } - for (const item of results) { - searchResultList.append(render(item)); + public static marker(match) { + return '<mark>' + match + '</mark>'; } - const endTime = performance.now(); + private async doSearch(keywords: string[]) { + const startTime = performance.now(); - searchResultTitle.innerText = `${results.length} pages (${((endTime - startTime) / 1000).toPrecision(1)} seconds)`; -} + const results = await this.searchKeywords(keywords); + this.clear(); -function marker(match) { - return '<mark>' + match + '</mark>'; -} + for (const item of results) { + this.list.append(Search.render(item)); + } -async function searchKeywords(keywords: string[]) { - const rawData = await getData(); - let results: pageData[] = []; + const endTime = performance.now(); - /// Sort keywords by their length - keywords.sort((a, b) => { - return b.length - a.length - }); + this.resultTitle.innerText = `${results.length} pages (${((endTime - startTime) / 1000).toPrecision(1)} seconds)`; + } - for (const item of rawData) { - let result = { - ...item, - preview: '', - matchCount: 0 + public async getData() { + if (!this.data) { + /// Not fetched yet + const jsonURL = this.form.dataset.json; + this.data = await fetch(jsonURL).then(res => res.json()); } - let matched = false; - - for (const keyword of keywords) { - if (keyword === '') continue; + return this.data; + } - const regex = new RegExp(escapeRegExp(replaceHTMLEnt(keyword)), 'gi'); + private bindSearchForm() { + let lastSearch = ''; - const contentMatch = regex.exec(result.content); - regex.lastIndex = 0; /// Reset regex + const eventHandler = (e) => { + e.preventDefault(); + const keywords = this.input.value; - const titleMatch = regex.exec(result.title); - regex.lastIndex = 0; /// Reset regex + Search.updateQueryString(keywords, true); - if (titleMatch) { - result.title = result.title.replace(regex, marker); + if (keywords === '') { + return this.clear(); } - if (titleMatch || contentMatch) { - matched = true; - ++result.matchCount; + if (lastSearch === keywords) return; + lastSearch = keywords; - let start = 0, - end = 100; + this.doSearch(keywords.split(' ')); + } - if (contentMatch) { - start = contentMatch.index - 20; - end = contentMatch.index + 80 + this.input.addEventListener('input', eventHandler); + this.input.addEventListener('compositionend', eventHandler); + } - if (start < 0) start = 0; - } + private clear() { + this.list.innerHTML = ''; + this.resultTitle.innerText = ''; + } - 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)} `; - } - } - } + 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 (matched) { - result.preview += '[...]'; - results.push(result); + if (keywords) { + this.doSearch(keywords.split(' ')); + } + else { + this.clear() } } - /** Result with more matches appears first */ - return results.sort((a, b) => { - return b.matchCount - a.matchCount; - }); -} + private static updateQueryString(keywords: string, replaceState = false) { + const pageURL = new URL(window.location.toString()); + + if (keywords === '') { + pageURL.searchParams.delete('keyword') + } + else { + pageURL.searchParams.set('keyword', keywords); + } -const render = (item: pageData) => { - return <article> - <a href={item.permalink}> - <div class="article-details"> - <h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2> - <secion class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></secion> - </div> - {item.image && - <div class="article-image"> - <img src={item.image} loading="lazy" /> + if (replaceState) { + window.history.replaceState('', '', pageURL.toString()); + } + else { + window.history.pushState('', '', pageURL.toString()); + } + } + + public static render(item: pageData) { + return <article> + <a href={item.permalink}> + <div class="article-details"> + <h2 class="article-title" dangerouslySetInnerHTML={{ __html: item.title }}></h2> + <secion class="article-preview" dangerouslySetInnerHTML={{ __html: item.preview }}></secion> </div> - } - </a> - </article>; + {item.image && + <div class="article-image"> + <img src={item.image} loading="lazy" /> + </div> + } + </a> + </article>; + } } -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 |