refactor(search): create Search class

This commit is contained in:
Jimmy Cai 2020-11-06 11:33:51 +01:00
parent b97e86a7a7
commit f5d45458fd

View File

@ -8,17 +8,6 @@ interface pageData {
matchCount: number 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 * Escape HTML tags as HTML entities
* Edited from: * Edited from:
@ -40,102 +29,30 @@ function replaceHTMLEnt(str) {
return str.replace(/[&<>"]/g, replaceTag); return str.replace(/[&<>"]/g, replaceTag);
} }
async function getData() { function escapeRegExp(string) {
if (!data) { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&');
/// Not fetched yet
const jsonURL = searchForm.dataset.json;
data = await fetch(jsonURL).then(res => res.json());
} }
return data; 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();
} }
function updateQueryString(keywords: string, replaceState = false) { private async searchKeywords(keywords: string[]) {
const pageURL = new URL(window.location.toString()); const rawData = await this.getData();
if (keywords === '') {
pageURL.searchParams.delete('keyword')
}
else {
pageURL.searchParams.set('keyword', keywords);
}
if (replaceState) {
window.history.replaceState('', '', pageURL.toString());
}
else {
window.history.pushState('', '', pageURL.toString());
}
}
function bindQueryStringChange() {
window.addEventListener('popstate', (e) => {
handleQueryString()
})
}
function handleQueryString() {
const pageURL = new URL(window.location.toString());
const keywords = pageURL.searchParams.get('keyword');
searchInput.value = keywords;
if (keywords) {
doSearch(keywords.split(' '));
}
else {
clear()
}
}
function bindSearchForm() {
let lastSearch = '';
const eventHandler = (e) => {
e.preventDefault();
const keywords = searchInput.value;
updateQueryString(keywords, true);
if (keywords === '') {
return clear();
}
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 '<mark>' + match + '</mark>';
}
async function searchKeywords(keywords: string[]) {
const rawData = await getData();
let results: pageData[] = []; let results: pageData[] = [];
/// Sort keywords by their length /// Sort keywords by their length
@ -164,7 +81,7 @@ async function searchKeywords(keywords: string[]) {
regex.lastIndex = 0; /// Reset regex regex.lastIndex = 0; /// Reset regex
if (titleMatch) { if (titleMatch) {
result.title = result.title.replace(regex, marker); result.title = result.title.replace(regex, Search.marker);
} }
if (titleMatch || contentMatch) { if (titleMatch || contentMatch) {
@ -182,11 +99,11 @@ async function searchKeywords(keywords: string[]) {
} }
if (result.preview.indexOf(keyword) !== -1) { if (result.preview.indexOf(keyword) !== -1) {
result.preview = result.preview.replace(regex, marker); result.preview = result.preview.replace(regex, Search.marker);
} }
else { else {
if (start !== 0) result.preview += `[...] `; if (start !== 0) result.preview += `[...] `;
result.preview += `${result.content.slice(start, end).replace(regex, marker)} `; result.preview += `${result.content.slice(start, end).replace(regex, Search.marker)} `;
} }
} }
} }
@ -203,7 +120,101 @@ async function searchKeywords(keywords: string[]) {
}); });
} }
const render = (item: pageData) => { public static marker(match) {
return '<mark>' + match + '</mark>';
}
private async doSearch(keywords: string[]) {
const startTime = performance.now();
const results = await this.searchKeywords(keywords);
this.clear();
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)`;
}
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;
}
private bindSearchForm() {
let lastSearch = '';
const eventHandler = (e) => {
e.preventDefault();
const keywords = this.input.value;
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 === '') {
pageURL.searchParams.delete('keyword')
}
else {
pageURL.searchParams.set('keyword', keywords);
}
if (replaceState) {
window.history.replaceState('', '', pageURL.toString());
}
else {
window.history.pushState('', '', pageURL.toString());
}
}
public static render(item: pageData) {
return <article> return <article>
<a href={item.permalink}> <a href={item.permalink}>
<div class="article-details"> <div class="article-details">
@ -218,9 +229,22 @@ const render = (item: pageData) => {
</a> </a>
</article>; </article>;
} }
}
window.addEventListener('DOMContentLoaded', () => { window.addEventListener('load', () => {
handleQueryString(); setTimeout(function () {
bindQueryStringChange(); const searchForm = document.querySelector('.search-form') as HTMLFormElement,
bindSearchForm(); 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;