🔧 🌱 Complete the local search feature.
This commit is contained in:
parent
6cbffe48b7
commit
9a4883832f
326
assets/js/third-party/search/local-search.js
vendored
Normal file
326
assets/js/third-party/search/local-search.js
vendored
Normal file
@ -0,0 +1,326 @@
|
|||||||
|
/* global CONFIG, pjax, LocalSearch */
|
||||||
|
class LocalSearch {
|
||||||
|
constructor({
|
||||||
|
path = '',
|
||||||
|
unescape = false,
|
||||||
|
top_n_per_article = 1
|
||||||
|
}) {
|
||||||
|
this.path = path;
|
||||||
|
this.unescape = unescape;
|
||||||
|
this.top_n_per_article = top_n_per_article;
|
||||||
|
this.isfetched = false;
|
||||||
|
this.datas = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
getIndexByWord(words, text, caseSensitive = false) {
|
||||||
|
const index = [];
|
||||||
|
const included = new Set();
|
||||||
|
|
||||||
|
if (!caseSensitive) {
|
||||||
|
text = text.toLowerCase();
|
||||||
|
}
|
||||||
|
words.forEach(word => {
|
||||||
|
if (this.unescape) {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.innerText = word;
|
||||||
|
word = div.innerHTML;
|
||||||
|
}
|
||||||
|
const wordLen = word.length;
|
||||||
|
if (wordLen === 0) return;
|
||||||
|
let startPosition = 0;
|
||||||
|
let position = -1;
|
||||||
|
if (!caseSensitive) {
|
||||||
|
word = word.toLowerCase();
|
||||||
|
}
|
||||||
|
while ((position = text.indexOf(word, startPosition)) > -1) {
|
||||||
|
index.push({ position, word });
|
||||||
|
included.add(word);
|
||||||
|
startPosition = position + wordLen;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
// Sort index by position of keyword
|
||||||
|
index.sort((left, right) => {
|
||||||
|
if (left.position !== right.position) {
|
||||||
|
return left.position - right.position;
|
||||||
|
}
|
||||||
|
return right.word.length - left.word.length;
|
||||||
|
});
|
||||||
|
return [index, included];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Merge hits into slices
|
||||||
|
mergeIntoSlice(start, end, index) {
|
||||||
|
let item = index[0];
|
||||||
|
let { position, word } = item;
|
||||||
|
const hits = [];
|
||||||
|
const count = new Set();
|
||||||
|
while (position + word.length <= end && index.length !== 0) {
|
||||||
|
count.add(word);
|
||||||
|
hits.push({
|
||||||
|
position,
|
||||||
|
length: word.length
|
||||||
|
});
|
||||||
|
const wordEnd = position + word.length;
|
||||||
|
|
||||||
|
// Move to next position of hit
|
||||||
|
index.shift();
|
||||||
|
while (index.length !== 0) {
|
||||||
|
item = index[0];
|
||||||
|
position = item.position;
|
||||||
|
word = item.word;
|
||||||
|
if (wordEnd > position) {
|
||||||
|
index.shift();
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
hits,
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
count: count.size
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight title and content
|
||||||
|
highlightKeyword(val, slice) {
|
||||||
|
let result = '';
|
||||||
|
let index = slice.start;
|
||||||
|
for (const { position, length } of slice.hits) {
|
||||||
|
result += val.substring(index, position);
|
||||||
|
index = position + length;
|
||||||
|
result += `<mark class="search-keyword">${val.substr(position, length)}</mark>`;
|
||||||
|
}
|
||||||
|
result += val.substring(index, slice.end);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
getResultItems(keywords) {
|
||||||
|
const resultItems = [];
|
||||||
|
this.datas.forEach(({ title, content, url }) => {
|
||||||
|
// The number of different keywords included in the article.
|
||||||
|
const [indexOfTitle, keysOfTitle] = this.getIndexByWord(keywords, title);
|
||||||
|
const [indexOfContent, keysOfContent] = this.getIndexByWord(keywords, content);
|
||||||
|
const includedCount = new Set([...keysOfTitle, ...keysOfContent]).size;
|
||||||
|
|
||||||
|
// Show search results
|
||||||
|
const hitCount = indexOfTitle.length + indexOfContent.length;
|
||||||
|
if (hitCount === 0) return;
|
||||||
|
|
||||||
|
const slicesOfTitle = [];
|
||||||
|
if (indexOfTitle.length !== 0) {
|
||||||
|
slicesOfTitle.push(this.mergeIntoSlice(0, title.length, indexOfTitle));
|
||||||
|
}
|
||||||
|
|
||||||
|
let slicesOfContent = [];
|
||||||
|
while (indexOfContent.length !== 0) {
|
||||||
|
const item = indexOfContent[0];
|
||||||
|
const { position } = item;
|
||||||
|
// Cut out 100 characters. The maxlength of .search-input is 80.
|
||||||
|
const start = Math.max(0, position - 20);
|
||||||
|
const end = Math.min(content.length, position + 80);
|
||||||
|
slicesOfContent.push(this.mergeIntoSlice(start, end, indexOfContent));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort slices in content by included keywords' count and hits' count
|
||||||
|
slicesOfContent.sort((left, right) => {
|
||||||
|
if (left.count !== right.count) {
|
||||||
|
return right.count - left.count;
|
||||||
|
} else if (left.hits.length !== right.hits.length) {
|
||||||
|
return right.hits.length - left.hits.length;
|
||||||
|
}
|
||||||
|
return left.start - right.start;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Select top N slices in content
|
||||||
|
const upperBound = parseInt(this.top_n_per_article, 10);
|
||||||
|
if (upperBound >= 0) {
|
||||||
|
slicesOfContent = slicesOfContent.slice(0, upperBound);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultItem = '';
|
||||||
|
|
||||||
|
url = new URL(url, location.origin);
|
||||||
|
url.searchParams.append('highlight', keywords.join(' '));
|
||||||
|
|
||||||
|
if (slicesOfTitle.length !== 0) {
|
||||||
|
resultItem += `<li><a href="${url.href}" class="search-result-title">${this.highlightKeyword(title, slicesOfTitle[0])}</a>`;
|
||||||
|
} else {
|
||||||
|
resultItem += `<li><a href="${url.href}" class="search-result-title">${title}</a>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
slicesOfContent.forEach(slice => {
|
||||||
|
resultItem += `<a href="${url.href}"><p class="search-result">${this.highlightKeyword(content, slice)}...</p></a>`;
|
||||||
|
});
|
||||||
|
|
||||||
|
resultItem += '</li>';
|
||||||
|
resultItems.push({
|
||||||
|
item: resultItem,
|
||||||
|
id : resultItems.length,
|
||||||
|
hitCount,
|
||||||
|
includedCount
|
||||||
|
});
|
||||||
|
});
|
||||||
|
return resultItems;
|
||||||
|
}
|
||||||
|
|
||||||
|
fetchData() {
|
||||||
|
const isXml = !this.path.endsWith('json');
|
||||||
|
fetch(this.path)
|
||||||
|
.then(response => response.text())
|
||||||
|
.then(res => {
|
||||||
|
// Get the contents from search data
|
||||||
|
this.isfetched = true;
|
||||||
|
this.datas = isXml ? [...new DOMParser().parseFromString(res, 'text/xml').querySelectorAll('entry')].map(element => ({
|
||||||
|
title : element.querySelector('title').textContent,
|
||||||
|
content: element.querySelector('content').textContent,
|
||||||
|
url : element.querySelector('url').textContent
|
||||||
|
})) : JSON.parse(res);
|
||||||
|
// Only match articles with non-empty titles
|
||||||
|
this.datas = this.datas.filter(data => data.title).map(data => {
|
||||||
|
data.title = data.title.trim();
|
||||||
|
data.content = data.content ? data.content.trim().replace(/<[^>]+>/g, '') : '';
|
||||||
|
data.url = decodeURIComponent(data.url).replace(/\/{2,}/g, '/');
|
||||||
|
return data;
|
||||||
|
});
|
||||||
|
// Remove loading animation
|
||||||
|
window.dispatchEvent(new Event('search:loaded'));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight by wrapping node in mark elements with the given class name
|
||||||
|
highlightText(node, slice, className) {
|
||||||
|
const val = node.nodeValue;
|
||||||
|
let index = slice.start;
|
||||||
|
const children = [];
|
||||||
|
for (const { position, length } of slice.hits) {
|
||||||
|
const text = document.createTextNode(val.substring(index, position));
|
||||||
|
index = position + length;
|
||||||
|
const mark = document.createElement('mark');
|
||||||
|
mark.className = className;
|
||||||
|
mark.appendChild(document.createTextNode(val.substr(position, length)));
|
||||||
|
children.push(text, mark);
|
||||||
|
}
|
||||||
|
node.nodeValue = val.substring(index, slice.end);
|
||||||
|
children.forEach(element => {
|
||||||
|
node.parentNode.insertBefore(element, node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Highlight the search words provided in the url in the text
|
||||||
|
highlightSearchWords(body) {
|
||||||
|
const params = new URL(location.href).searchParams.get('highlight');
|
||||||
|
const keywords = params ? params.split(' ') : [];
|
||||||
|
if (!keywords.length || !body) return;
|
||||||
|
const walk = document.createTreeWalker(body, NodeFilter.SHOW_TEXT, null);
|
||||||
|
const allNodes = [];
|
||||||
|
while (walk.nextNode()) {
|
||||||
|
if (!walk.currentNode.parentNode.matches('button, select, textarea')) allNodes.push(walk.currentNode);
|
||||||
|
}
|
||||||
|
allNodes.forEach(node => {
|
||||||
|
const [indexOfNode] = this.getIndexByWord(keywords, node.nodeValue);
|
||||||
|
if (!indexOfNode.length) return;
|
||||||
|
const slice = this.mergeIntoSlice(0, node.nodeValue.length, indexOfNode);
|
||||||
|
this.highlightText(node, slice, 'search-keyword');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (! NexT.CONFIG.localSearch.path) {
|
||||||
|
// Search DB path
|
||||||
|
console.warn('`search indexes file` is not configurate!');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const localSearch = new LocalSearch({
|
||||||
|
path : NexT.CONFIG.localSearch.path,
|
||||||
|
top_n_per_article: NexT.CONFIG.localSearch.topnperarticle,
|
||||||
|
unescape : NexT.CONFIG.localSearch.unescape
|
||||||
|
});
|
||||||
|
|
||||||
|
const input = document.querySelector('.search-input');
|
||||||
|
|
||||||
|
const inputEventFunction = () => {
|
||||||
|
if (!localSearch.isfetched) return;
|
||||||
|
const searchText = input.value.trim().toLowerCase();
|
||||||
|
const keywords = searchText.split(/[-\s]+/);
|
||||||
|
const container = document.querySelector('.search-result-container');
|
||||||
|
let resultItems = [];
|
||||||
|
if (searchText.length > 0) {
|
||||||
|
// Perform local searching
|
||||||
|
resultItems = localSearch.getResultItems(keywords);
|
||||||
|
}
|
||||||
|
if (keywords.length === 1 && keywords[0] === '') {
|
||||||
|
container.classList.add('no-result');
|
||||||
|
container.innerHTML = '<div class="search-result-icon"><i class="fa fa-search fa-5x"></i></div>';
|
||||||
|
} else if (resultItems.length === 0) {
|
||||||
|
container.classList.add('no-result');
|
||||||
|
container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>';
|
||||||
|
} else {
|
||||||
|
resultItems.sort((left, right) => {
|
||||||
|
if (left.includedCount !== right.includedCount) {
|
||||||
|
return right.includedCount - left.includedCount;
|
||||||
|
} else if (left.hitCount !== right.hitCount) {
|
||||||
|
return right.hitCount - left.hitCount;
|
||||||
|
}
|
||||||
|
return right.id - left.id;
|
||||||
|
});
|
||||||
|
const stats = NexT.CONFIG.i18n.hits.replace('${hits}', resultItems.length);
|
||||||
|
|
||||||
|
container.classList.remove('no-result');
|
||||||
|
container.innerHTML = `<div class="search-stats">${stats}</div>
|
||||||
|
<hr>
|
||||||
|
<ul class="search-result-list">${resultItems.map(result => result.item).join('')}</ul>`;
|
||||||
|
if (typeof pjax === 'object') pjax.refresh(container);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
localSearch.highlightSearchWords(document.querySelector('.post-body'));
|
||||||
|
if (NexT.CONFIG.localSearch.preload) {
|
||||||
|
localSearch.fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NexT.CONFIG.localSearch.trigger === 'auto') {
|
||||||
|
input.addEventListener('input', inputEventFunction);
|
||||||
|
} else {
|
||||||
|
document.querySelector('.search-icon').addEventListener('click', inputEventFunction);
|
||||||
|
input.addEventListener('keypress', event => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
inputEventFunction();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
window.addEventListener('search:loaded', inputEventFunction);
|
||||||
|
|
||||||
|
// Handle and trigger popup window
|
||||||
|
document.querySelectorAll('.popup-trigger').forEach(element => {
|
||||||
|
element.addEventListener('click', () => {
|
||||||
|
document.body.classList.add('search-active');
|
||||||
|
// Wait for search-popup animation to complete
|
||||||
|
setTimeout(() => input.focus(), 500);
|
||||||
|
if (!localSearch.isfetched) localSearch.fetchData();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Monitor main search box
|
||||||
|
const onPopupClose = () => {
|
||||||
|
document.body.classList.remove('search-active');
|
||||||
|
};
|
||||||
|
|
||||||
|
document.querySelector('.search-pop-overlay').addEventListener('click', event => {
|
||||||
|
if (event.target === document.querySelector('.search-pop-overlay')) {
|
||||||
|
onPopupClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose);
|
||||||
|
document.addEventListener('pjax:success', () => {
|
||||||
|
localSearch.highlightSearchWords(document.querySelector('.post-body'));
|
||||||
|
onPopupClose();
|
||||||
|
});
|
||||||
|
window.addEventListener('keyup', event => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
onPopupClose();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
@ -55,6 +55,27 @@ markup:
|
|||||||
endLevel: 3
|
endLevel: 3
|
||||||
ordered: false
|
ordered: false
|
||||||
|
|
||||||
|
# 站点文章导航文件
|
||||||
|
# Site map of all articles
|
||||||
|
sitemap:
|
||||||
|
filename: "sitemap.xml"
|
||||||
|
changefreq: "weekly"
|
||||||
|
priority: 0.5
|
||||||
|
|
||||||
|
outputFormats:
|
||||||
|
RSS:
|
||||||
|
baseName: "rss"
|
||||||
|
# 自定义生成本地搜索文件
|
||||||
|
# Custom file of indexes for local search
|
||||||
|
SearchIndexes:
|
||||||
|
mediaType: "application/xml"
|
||||||
|
baseName: "searchindexes"
|
||||||
|
isPlainText: true
|
||||||
|
notAlternative: true
|
||||||
|
|
||||||
|
outputs:
|
||||||
|
home: ["HTML", "RSS", "SearchIndexes"]
|
||||||
|
|
||||||
#--------------------------------------
|
#--------------------------------------
|
||||||
# 菜单配置说明
|
# 菜单配置说明
|
||||||
# identifier : 唯一标识不可重复
|
# identifier : 唯一标识不可重复
|
||||||
@ -889,16 +910,26 @@ params:
|
|||||||
hits:
|
hits:
|
||||||
perPage: 10
|
perPage: 10
|
||||||
|
|
||||||
|
# 本地搜索
|
||||||
# Local Search
|
# Local Search
|
||||||
localSearch:
|
localSearch:
|
||||||
|
# 是否开启搜索功能
|
||||||
|
# Enable search function
|
||||||
enable: true
|
enable: true
|
||||||
|
# 搜索索引文件路径
|
||||||
|
# Indexes file path for search
|
||||||
|
path: searchindexes.xml
|
||||||
|
# 是立即搜索当输入关键字时,可选值: auto | manual
|
||||||
# If auto, trigger search by changing input.
|
# If auto, trigger search by changing input.
|
||||||
# If manual, trigger search by pressing enter key or search button.
|
# If manual, trigger search by pressing enter key or search button.
|
||||||
trigger: auto
|
trigger: auto
|
||||||
|
# 显示头部的搜索记录,-1 表示显示所有搜索结果
|
||||||
# Show top n results per article, show all results by setting to -1
|
# Show top n results per article, show all results by setting to -1
|
||||||
top_n_per_article: 1
|
topNPerArticle: -1
|
||||||
|
# 将 html 字符串转换为可读字符串
|
||||||
# Unescape html strings to the readable one.
|
# Unescape html strings to the readable one.
|
||||||
unescape: false
|
unescape: false
|
||||||
|
# 页面加载时是否要重新载入索引文件
|
||||||
# Preload the search data when the page loads.
|
# Preload the search data when the page loads.
|
||||||
preload: false
|
preload: false
|
||||||
|
|
||||||
|
@ -141,3 +141,12 @@ PageViewsLabel:
|
|||||||
|
|
||||||
FooterPowerby:
|
FooterPowerby:
|
||||||
other: "Power by %s"
|
other: "Power by %s"
|
||||||
|
|
||||||
|
SearchPh:
|
||||||
|
other: Searching...
|
||||||
|
SearchEmpty:
|
||||||
|
other: "We didn't find any results for the search: ${query}"
|
||||||
|
SearchHits:
|
||||||
|
hits: "${hits} results found"
|
||||||
|
SearchHitsTime:
|
||||||
|
other: "${hits} results found in ${time} ms"
|
@ -142,3 +142,12 @@ PageViewsLabel:
|
|||||||
|
|
||||||
FooterPowerby:
|
FooterPowerby:
|
||||||
other: 由 %s 强力驱动
|
other: 由 %s 强力驱动
|
||||||
|
|
||||||
|
SearchPh:
|
||||||
|
other: 搜索...
|
||||||
|
SearchEmpty:
|
||||||
|
other: "没有找到任何搜索结果:${query}"
|
||||||
|
SearchHits:
|
||||||
|
hits: "找到 ${hits} 个搜索结果"
|
||||||
|
SearchHitsTime:
|
||||||
|
other: "找到 ${hits} 个搜索结果(用时 ${time} 毫秒)"
|
18
layouts/_default/list.searchindexes.xml
Normal file
18
layouts/_default/list.searchindexes.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
{{ printf "<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?>" | safeHTML }}
|
||||||
|
<search>
|
||||||
|
{{range where .Site.RegularPages "Kind" "page"}}
|
||||||
|
<entry>
|
||||||
|
<title>{{ .Title }}</title>
|
||||||
|
<url>{{ .Permalink }}</url>
|
||||||
|
<categories>
|
||||||
|
{{- range .Params.categories }}<category>{{ . }}</category>{{- end }}
|
||||||
|
</categories>
|
||||||
|
<tags>
|
||||||
|
{{- range .Params.tags }}
|
||||||
|
<tag>{{ . }}</tag>
|
||||||
|
{{- end }}
|
||||||
|
</tags>
|
||||||
|
<content type="html"><![CDATA[{{ .Content | plainify }}]]></content>
|
||||||
|
</entry>
|
||||||
|
{{ end }}
|
||||||
|
</search>
|
14
layouts/partials/_thirdparty/search/algolia.html
vendored
Normal file
14
layouts/partials/_thirdparty/search/algolia.html
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<div class="search-header">
|
||||||
|
<span class="search-icon">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</span>
|
||||||
|
<div class="search-input-container"></div>
|
||||||
|
<span class="popup-btn-close" role="button">
|
||||||
|
<i class="fa fa-times-circle"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="search-result-container">
|
||||||
|
<div class="algolia-stats"><hr></div>
|
||||||
|
<div class="algolia-hits"></div>
|
||||||
|
<div class="algolia-pagination"></div>
|
||||||
|
</div>
|
18
layouts/partials/_thirdparty/search/local.html
vendored
Normal file
18
layouts/partials/_thirdparty/search/local.html
vendored
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<div class="search-header">
|
||||||
|
<span class="search-icon">
|
||||||
|
<i class="fa fa-search"></i>
|
||||||
|
</span>
|
||||||
|
<div class="search-input-container">
|
||||||
|
<input autocomplete="off" autocapitalize="off" maxlength="80"
|
||||||
|
placeholder="{{ T "SearchPh" }}" spellcheck="false"
|
||||||
|
type="search" class="search-input">
|
||||||
|
</div>
|
||||||
|
<span class="popup-btn-close" role="button">
|
||||||
|
<i class="fa fa-times-circle"></i>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="search-result-container no-result">
|
||||||
|
<div class="search-result-icon">
|
||||||
|
<i class="fa fa-spinner fa-pulse fa-5x"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
@ -1,2 +1,3 @@
|
|||||||
{{ partial "header/brand.html" . }}
|
{{ partial "header/brand.html" . }}
|
||||||
{{ partial "header/menus.html" . }}
|
{{ partial "header/menus.html" . }}
|
||||||
|
{{ partial "header/search.html" . }}
|
11
layouts/partials/header/search.html
Normal file
11
layouts/partials/header/search.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{{- if or .Site.Params.algoliaSearch.enable .Site.Params.localSearch.enable }}
|
||||||
|
<div class="search-pop-overlay">
|
||||||
|
<div class="popup search-popup">
|
||||||
|
{{- if .Site.Params.algoliaSearch.enable }}
|
||||||
|
{{ partial "_thirdparty/search/algolia.html" . }}
|
||||||
|
{{- else if .Site.Params.localSearch.enable }}
|
||||||
|
{{ partial "_thirdparty/search/local.html" . }}
|
||||||
|
{{- end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{- end }}
|
@ -23,18 +23,25 @@
|
|||||||
{{ $globalVars.Set "router" $router }}
|
{{ $globalVars.Set "router" $router }}
|
||||||
|
|
||||||
{{ $config := dict
|
{{ $config := dict
|
||||||
"hostname" .Site.BaseURL
|
"hostname" .Site.BaseURL
|
||||||
"root" "/"
|
"root" "/"
|
||||||
"lang" .Site.LanguageCode
|
"lang" .Site.LanguageCode
|
||||||
"vendor" (dict "plugins" $vendor "router" $router)
|
"vendor" (dict "plugins" $vendor "router" $router)
|
||||||
"darkmode" .Site.Params.darkmode
|
"darkmode" .Site.Params.darkmode
|
||||||
"version" .Site.Data.config.version
|
"version" .Site.Data.config.version
|
||||||
"scheme" .Site.Params.scheme
|
"scheme" .Site.Params.scheme
|
||||||
"sidebar" .Site.Params.sidebar
|
"sidebar" .Site.Params.sidebar
|
||||||
"copybtn" .Site.Params.codeblock.copyBtn
|
"copybtn" .Site.Params.codeblock.copyBtn
|
||||||
"bookmark" .Site.Params.bookmark
|
"bookmark" .Site.Params.bookmark
|
||||||
"lazyload" .Site.Params.lazyload
|
"lazyload" .Site.Params.lazyload
|
||||||
"motion" .Site.Params.motion
|
"motion" .Site.Params.motion
|
||||||
|
"localSearch" .Site.Params.localSearch
|
||||||
|
"i18n" (dict
|
||||||
|
"placeholder" (T "SearchPh")
|
||||||
|
"empty" (T "SearchEmpty")
|
||||||
|
"hits_time" (T "SearchHitsTime")
|
||||||
|
"hits" (T "SearchHits")
|
||||||
|
)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
{{ with .Site.Params.waline }}
|
{{ with .Site.Params.waline }}
|
||||||
|
@ -69,3 +69,6 @@
|
|||||||
{{- $nextjs = $nextjs | minify | fingerprint }}
|
{{- $nextjs = $nextjs | minify | fingerprint }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
<script type="text/javascript" src="{{ $nextjs.RelPermalink }}" defer></script>
|
<script type="text/javascript" src="{{ $nextjs.RelPermalink }}" defer></script>
|
||||||
|
{{- $search := resources.Get "js/third-party/search/local-search.js" }}
|
||||||
|
<script type="text/javascript" src="{{ $search.RelPermalink }}" defer></script>
|
||||||
|
|
||||||
|
99
static/js/third-party/search/local-search.js
vendored
99
static/js/third-party/search/local-search.js
vendored
@ -1,99 +0,0 @@
|
|||||||
/* global CONFIG, pjax, LocalSearch */
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
if (!CONFIG.path) {
|
|
||||||
// Search DB path
|
|
||||||
console.warn('`hexo-generator-searchdb` plugin is not installed!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const localSearch = new LocalSearch({
|
|
||||||
path : CONFIG.path,
|
|
||||||
top_n_per_article: CONFIG.localsearch.top_n_per_article,
|
|
||||||
unescape : CONFIG.localsearch.unescape
|
|
||||||
});
|
|
||||||
|
|
||||||
const input = document.querySelector('.search-input');
|
|
||||||
|
|
||||||
const inputEventFunction = () => {
|
|
||||||
if (!localSearch.isfetched) return;
|
|
||||||
const searchText = input.value.trim().toLowerCase();
|
|
||||||
const keywords = searchText.split(/[-\s]+/);
|
|
||||||
const container = document.querySelector('.search-result-container');
|
|
||||||
let resultItems = [];
|
|
||||||
if (searchText.length > 0) {
|
|
||||||
// Perform local searching
|
|
||||||
resultItems = localSearch.getResultItems(keywords);
|
|
||||||
}
|
|
||||||
if (keywords.length === 1 && keywords[0] === '') {
|
|
||||||
container.classList.add('no-result');
|
|
||||||
container.innerHTML = '<div class="search-result-icon"><i class="fa fa-search fa-5x"></i></div>';
|
|
||||||
} else if (resultItems.length === 0) {
|
|
||||||
container.classList.add('no-result');
|
|
||||||
container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>';
|
|
||||||
} else {
|
|
||||||
resultItems.sort((left, right) => {
|
|
||||||
if (left.includedCount !== right.includedCount) {
|
|
||||||
return right.includedCount - left.includedCount;
|
|
||||||
} else if (left.hitCount !== right.hitCount) {
|
|
||||||
return right.hitCount - left.hitCount;
|
|
||||||
}
|
|
||||||
return right.id - left.id;
|
|
||||||
});
|
|
||||||
const stats = CONFIG.i18n.hits.replace('${hits}', resultItems.length);
|
|
||||||
|
|
||||||
container.classList.remove('no-result');
|
|
||||||
container.innerHTML = `<div class="search-stats">${stats}</div>
|
|
||||||
<hr>
|
|
||||||
<ul class="search-result-list">${resultItems.map(result => result.item).join('')}</ul>`;
|
|
||||||
if (typeof pjax === 'object') pjax.refresh(container);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
localSearch.highlightSearchWords(document.querySelector('.post-body'));
|
|
||||||
if (CONFIG.localsearch.preload) {
|
|
||||||
localSearch.fetchData();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (CONFIG.localsearch.trigger === 'auto') {
|
|
||||||
input.addEventListener('input', inputEventFunction);
|
|
||||||
} else {
|
|
||||||
document.querySelector('.search-icon').addEventListener('click', inputEventFunction);
|
|
||||||
input.addEventListener('keypress', event => {
|
|
||||||
if (event.key === 'Enter') {
|
|
||||||
inputEventFunction();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
window.addEventListener('search:loaded', inputEventFunction);
|
|
||||||
|
|
||||||
// Handle and trigger popup window
|
|
||||||
document.querySelectorAll('.popup-trigger').forEach(element => {
|
|
||||||
element.addEventListener('click', () => {
|
|
||||||
document.body.classList.add('search-active');
|
|
||||||
// Wait for search-popup animation to complete
|
|
||||||
setTimeout(() => input.focus(), 500);
|
|
||||||
if (!localSearch.isfetched) localSearch.fetchData();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Monitor main search box
|
|
||||||
const onPopupClose = () => {
|
|
||||||
document.body.classList.remove('search-active');
|
|
||||||
};
|
|
||||||
|
|
||||||
document.querySelector('.search-pop-overlay').addEventListener('click', event => {
|
|
||||||
if (event.target === document.querySelector('.search-pop-overlay')) {
|
|
||||||
onPopupClose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
document.querySelector('.popup-btn-close').addEventListener('click', onPopupClose);
|
|
||||||
document.addEventListener('pjax:success', () => {
|
|
||||||
localSearch.highlightSearchWords(document.querySelector('.post-body'));
|
|
||||||
onPopupClose();
|
|
||||||
});
|
|
||||||
window.addEventListener('keyup', event => {
|
|
||||||
if (event.key === 'Escape') {
|
|
||||||
onPopupClose();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
@ -1,4 +1,4 @@
|
|||||||
name = "Hugo Next"
|
name = "Hugo NexT"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
licenselink = "https://github.com/hugo-next/hugo-theme-next/blob/main/LICENSE"
|
licenselink = "https://github.com/hugo-next/hugo-theme-next/blob/main/LICENSE"
|
||||||
description = "Easily & powerful theme for Hugo engine."
|
description = "Easily & powerful theme for Hugo engine."
|
||||||
|
Loading…
Reference in New Issue
Block a user