hugo-theme-stack/assets/ts/archives.ts
Jimmy Cai 5d100a7f99
feat: new archives layout (#989)
* feat(archives): add search bar to archives page

* fix: error `file is nil; wrap it in if or with` from Hugo 0.123.0

* style: rename `search.ts` to `archives.ts`

* refactor: remove `search` page layout

* doc: remove `search` from `exampleSite`

* fix: generate JSON output for archives page in exampleSite

* feat: put archives search form under categories list
2024-03-25 00:02:47 +01:00

165 lines
5.1 KiB
TypeScript

interface PageData {
title: string;
content: string;
id: string;
}
class Search {
private data: PageData[];
private form: HTMLFormElement;
private input: HTMLInputElement;
private resultTitle: HTMLHeadElement;
private resultTitleTemplate: string;
constructor({ form, input, resultTitle, resultTitleTemplate }) {
this.form = form;
this.input = input;
this.resultTitle = resultTitle;
this.resultTitleTemplate = resultTitleTemplate;
this.handleQueryString();
this.bindQueryStringChange();
this.bindSearchForm();
}
private async getIndex() {
if (!this.data) {
const jsonURL = this.form.dataset.json;
this.data = await fetch(jsonURL).then(res => res.json());
}
return this.data;
}
private async searchKeywords(keywords: string[]) {
const index = await this.getIndex();
/// Return an set of ids that match the keywords
return new Set(index.filter(item => {
return keywords.every(keyword => {
return item.title.includes(keyword) || item.content.includes(keyword);
});
}).map(item => item.id));
}
private async doSearch(keywords: string[]) {
const startTime = performance.now();
const results = await this.searchKeywords(keywords);
this.clear();
/// Hide all articles except the ones that are in the results
const archiveGroups = document.querySelectorAll('.archives-group') as NodeListOf<HTMLDivElement>;
archiveGroups.forEach(group => {
const articles = Array.from(group.querySelectorAll('article'));
articles.map(article => {
article.style.display = 'none';
article.style.removeProperty('border-bottom');
});
const matchingArticles = articles.filter(article => results.has(article.id));
const hasResults = matchingArticles.length > 0;
if (!hasResults) return group.style.display = 'none';
matchingArticles.map(article => article.style.removeProperty('display'));
matchingArticles[matchingArticles.length - 1].style.borderBottom = 'none';
});
const endTime = performance.now();
this.resultTitle.innerText = this.generateResultTitle(results.size, ((endTime - startTime) / 1000).toPrecision(1));
this.resultTitle.style.display = 'block';
}
private generateResultTitle(resultLen, time) {
return this.resultTitleTemplate.replace("#PAGES_COUNT", resultLen).replace("#TIME_SECONDS", time);
}
private bindSearchForm() {
let lastSearch = '';
const eventHandler = (e) => {
e.preventDefault();
const keywords = this.input.value.trim();
Search.updateQueryString(keywords, true);
if (keywords === '') {
lastSearch = '';
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.resultTitle.style.display = 'none';
document.querySelectorAll('.archives-group, .archives-group article').forEach(el => el.removeAttribute('style'));
}
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());
}
}
}
declare global {
interface Window {
searchResultTitleTemplate: string;
}
}
window.addEventListener('load', () => {
setTimeout(function () {
const searchForm = document.getElementById('search-form') as HTMLFormElement,
searchInput = searchForm.querySelector('input') as HTMLInputElement,
searchResultTitle = document.querySelector('.search-result--title') as HTMLHeadingElement;
new Search({
form: searchForm,
input: searchInput,
resultTitle: searchResultTitle,
resultTitleTemplate: window.searchResultTitleTemplate
});
}, 0);
})
export default Search;