mirror of
https://github.com/CaiJimmy/hugo-theme-stack.git
synced 2025-04-28 19:43:31 +08:00
feat(archives): add search bar to archives page
This commit is contained in:
parent
4d6b65f63c
commit
4a607c1bff
165
assets/ts/search.ts
Normal file
165
assets/ts/search.ts
Normal file
@ -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;
|
@ -1,9 +1,27 @@
|
||||
{{ define "body-class" }}template-archives{{ end }}
|
||||
{{ define "body-class" }}template-archives template-search{{ end }}
|
||||
{{ define "head" }}
|
||||
{{- with .OutputFormats.Get "json" -}}
|
||||
<link rel="preload" href="{{ .RelPermalink }}" as="fetch" crossorigin="anonymous">
|
||||
{{- end -}}
|
||||
{{ end }}
|
||||
{{ define "main" }}
|
||||
<form id="search-form" action="{{ .RelPermalink }}" class="search-form"{{ with .OutputFormats.Get "json" -}} data-json="{{ .RelPermalink }}"{{- end }}>
|
||||
<p>
|
||||
<label>{{ T "search.title" }}</label>
|
||||
<input name="keyword" placeholder="{{ T `search.placeholder` }}" />
|
||||
</p>
|
||||
|
||||
<button title="{{ T `search.title` }}">
|
||||
{{ partial "helper/icon" "search" }}
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<h3 class="search-result--title section-title">dawdwa</h3>
|
||||
|
||||
{{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}}
|
||||
{{- $terms := $taxonomy.Pages -}}
|
||||
{{ if $terms }}
|
||||
<header>
|
||||
{{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}}
|
||||
{{- $terms := $taxonomy.Pages -}}
|
||||
{{ if $terms }}
|
||||
<h2 class="section-title">{{ $taxonomy.Title }}</h2>
|
||||
<div class="subsection-list">
|
||||
<div class="article-list--tile">
|
||||
@ -12,8 +30,8 @@
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
{{ end }}
|
||||
</header>
|
||||
{{ end }}
|
||||
|
||||
{{ $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }}
|
||||
|
||||
@ -30,4 +48,16 @@
|
||||
{{ end }}
|
||||
|
||||
{{ partialCached "footer/footer" . }}
|
||||
|
||||
<script>
|
||||
window.searchResultTitleTemplate = "{{ T `search.resultTitle` }}"
|
||||
</script>
|
||||
|
||||
{{- $opts := dict "minify" hugo.IsProduction -}}
|
||||
{{- $searchScript := resources.Get "ts/search.ts" | js.Build $opts -}}
|
||||
<script type="text/javascript" src="{{ $searchScript.RelPermalink }}" defer></script>
|
||||
{{ end }}
|
||||
|
||||
{{ define "right-sidebar" }}
|
||||
{{ partial "sidebar/right.html" (dict "Context" . "Scope" "homepage") }}
|
||||
{{ end }}
|
13
layouts/_default/archives.json
Normal file
13
layouts/_default/archives.json
Normal file
@ -0,0 +1,13 @@
|
||||
{{- $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections -}}
|
||||
{{- $result := slice -}}
|
||||
|
||||
{{- range $pages -}}
|
||||
{{- $data := dict
|
||||
"title" .Title
|
||||
"content" (.Plain)
|
||||
"id" .File.UniqueID
|
||||
-}}
|
||||
{{- $result = $result | append $data -}}
|
||||
{{- end -}}
|
||||
|
||||
{{ jsonify $result }}
|
@ -1,4 +1,4 @@
|
||||
<article>
|
||||
<article id="{{ .File.UniqueID }}">
|
||||
<a href="{{ .RelPermalink }}">
|
||||
<div class="article-details">
|
||||
<h2 class="article-title">
|
||||
|
Loading…
Reference in New Issue
Block a user