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
This commit is contained in:
@@ -0,0 +1,165 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user