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" }}
|
{{ 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>
|
<header>
|
||||||
{{- $taxonomy := $.Site.GetPage "taxonomyTerm" "categories" -}}
|
|
||||||
{{- $terms := $taxonomy.Pages -}}
|
|
||||||
{{ if $terms }}
|
|
||||||
<h2 class="section-title">{{ $taxonomy.Title }}</h2>
|
<h2 class="section-title">{{ $taxonomy.Title }}</h2>
|
||||||
<div class="subsection-list">
|
<div class="subsection-list">
|
||||||
<div class="article-list--tile">
|
<div class="article-list--tile">
|
||||||
@ -12,8 +30,8 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{ end }}
|
|
||||||
</header>
|
</header>
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
{{ $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }}
|
{{ $pages := where .Site.RegularPages "Type" "in" .Site.Params.mainSections }}
|
||||||
|
|
||||||
@ -30,4 +48,16 @@
|
|||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
{{ partialCached "footer/footer" . }}
|
{{ 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 }}
|
{{ 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 }}">
|
<a href="{{ .RelPermalink }}">
|
||||||
<div class="article-details">
|
<div class="article-details">
|
||||||
<h2 class="article-title">
|
<h2 class="article-title">
|
||||||
|
Loading…
Reference in New Issue
Block a user