mirror of
https://github.com/next-theme/hexo-theme-next.git
synced 2026-01-18 18:33:42 +00:00
Refactor Algolia search: drop instantsearch.js (#795)
This commit is contained in:
parent
263841bd7a
commit
1ca10776ff
@ -121,11 +121,6 @@ algolia_search:
|
||||
version: 4.23.3
|
||||
file: dist/algoliasearch-lite.umd.js
|
||||
integrity: sha256-1QNshz86RqXe/qsCBldsUu13eAX6n/O98uubKQs87UI=
|
||||
instant_search:
|
||||
name: instantsearch.js
|
||||
version: 4.67.0
|
||||
file: dist/instantsearch.production.min.js
|
||||
integrity: sha256-TW7D3X/i/W+RUgEeDppEnFT2ixv5lzplKH0c58D92dY=
|
||||
local_search:
|
||||
name: hexo-generator-searchdb
|
||||
version: 1.4.1
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
<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>
|
||||
@ -1,11 +1,24 @@
|
||||
{%- if theme.algolia_search.enable or theme.local_search.enable %}
|
||||
<div class="search-pop-overlay">
|
||||
<div class="popup search-popup">
|
||||
{%- if theme.algolia_search.enable %}
|
||||
{%- include 'algolia-search.njk' -%}
|
||||
{% elif theme.local_search.enable %}
|
||||
{%- include 'localsearch.njk' -%}
|
||||
{%- endif %}
|
||||
<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="{{ __('search.placeholder') }}" 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">
|
||||
<div class="search-result-icon">
|
||||
<i class="{% if theme.algolia_search.enable %}fab fa-algolia{% elif theme.local_search.enable %}fa fa-spinner fa-pulse{% endif %} fa-5x"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{%- endif %}
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
<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="{{ __('search.placeholder') }}" 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,4 +1,3 @@
|
||||
{{ next_vendors('algolia_search') }}
|
||||
{{ next_vendors('instant_search') }}
|
||||
|
||||
{{- next_js('third-party/search/algolia-search.js') }}
|
||||
|
||||
@ -79,11 +79,14 @@ if (hexo-config('local_search.enable') or hexo-config('algolia_search.enable'))
|
||||
}
|
||||
|
||||
.search-result-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: calc(100% - 55px);
|
||||
overflow: auto;
|
||||
padding: 5px 25px;
|
||||
|
||||
hr {
|
||||
flex-shrink: 0;
|
||||
margin: 5px 0 10px;
|
||||
|
||||
&:first-child {
|
||||
@ -105,18 +108,27 @@ if (hexo-config('local_search.enable') or hexo-config('algolia_search.enable'))
|
||||
border-bottom: 1px dashed $grey-light;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.search-input-container {
|
||||
flex-grow: 1;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.search-result-icon {
|
||||
color: $grey-light;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mark.search-keyword {
|
||||
background: transparent;
|
||||
border-bottom: 1px dashed $red;
|
||||
color: $red;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
if (hexo-config('algolia_search.enable')) {
|
||||
.search-input-container {
|
||||
flex-grow: 1;
|
||||
|
||||
form {
|
||||
padding: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.search-stats {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
@ -128,52 +140,13 @@ if (hexo-config('algolia_search.enable')) {
|
||||
}
|
||||
}
|
||||
|
||||
.algolia-pagination {
|
||||
// Override default style of ul
|
||||
margin: 40px 0;
|
||||
opacity: 1;
|
||||
padding: 0;
|
||||
|
||||
.pagination-item {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.current .page-number {
|
||||
@extend $page-number-current;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.disabled-item {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hexo-config('local_search.enable')) {
|
||||
.search-popup {
|
||||
.search-input-container {
|
||||
flex-grow: 1;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.no-result {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.search-result-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-result-icon {
|
||||
color: $grey-light;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
mark.search-keyword {
|
||||
background: transparent;
|
||||
border-bottom: 1px dashed $red;
|
||||
color: $red;
|
||||
font-weight: bold;
|
||||
.pagination.algolia-pagination {
|
||||
// Override the default style of pagination
|
||||
// Put pagination at the bottom when there is sufficient height
|
||||
margin-top: auto;
|
||||
padding: 40px 0;
|
||||
// Override motion
|
||||
// https://github.com/theme-next/hexo-theme-next/issues/537
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.8 KiB |
176
source/js/third-party/search/algolia-search.js
vendored
176
source/js/third-party/search/algolia-search.js
vendored
@ -1,112 +1,104 @@
|
||||
/* global instantsearch, algoliasearch, CONFIG, pjax */
|
||||
/* global CONFIG, NexT, pjax, algoliasearch */
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const { indexName, appID, apiKey, hits } = CONFIG.algolia;
|
||||
const client = algoliasearch(appID, apiKey);
|
||||
const index = client.initIndex(indexName);
|
||||
|
||||
const search = instantsearch({
|
||||
indexName,
|
||||
searchClient : algoliasearch(appID, apiKey),
|
||||
searchFunction: helper => {
|
||||
if (document.querySelector('.search-input').value) {
|
||||
helper.search();
|
||||
}
|
||||
const input = document.querySelector('.search-input');
|
||||
const container = document.querySelector('.search-result-container');
|
||||
|
||||
const formatHits = data => {
|
||||
const { title, excerpt, excerptStrip, contentStripTruncate } = data._highlightResult;
|
||||
let result = `<li><a href="${data.permalink}" class="search-result-title">${title.value}</a>`;
|
||||
const content = excerpt || excerptStrip || contentStripTruncate;
|
||||
if (content && content.value) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = content.value;
|
||||
result += `<a href="${data.permalink}"><p class="search-result">${div.textContent.substring(0, 100)}...</p></a></li>`;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
if (typeof pjax === 'object') {
|
||||
search.on('render', () => {
|
||||
pjax.refresh(document.querySelector('.algolia-hits'));
|
||||
let isSearching = false;
|
||||
let pendingQuery = null;
|
||||
|
||||
const searchAlgolia = async(searchText, page = 0) => {
|
||||
if (isSearching) {
|
||||
pendingQuery = { searchText, page };
|
||||
return;
|
||||
}
|
||||
isSearching = true;
|
||||
const startTime = Date.now();
|
||||
const data = await index.search(searchText, {
|
||||
page,
|
||||
attributesToRetrieve : ['permalink'],
|
||||
attributesToHighlight: ['title', 'excerpt', 'excerptStrip', 'contentStripTruncate'],
|
||||
hitsPerPage : hits.per_page || 10,
|
||||
highlightPreTag : '<mark class="search-keyword">',
|
||||
highlightPostTag : '</mark>'
|
||||
});
|
||||
}
|
||||
|
||||
// Registering Widgets
|
||||
search.addWidgets([
|
||||
instantsearch.widgets.configure({
|
||||
hitsPerPage: hits.per_page || 10
|
||||
}),
|
||||
|
||||
instantsearch.widgets.searchBox({
|
||||
container : '.search-input-container',
|
||||
placeholder : CONFIG.i18n.placeholder,
|
||||
// Hide default icons of algolia search
|
||||
showReset : false,
|
||||
showSubmit : false,
|
||||
showLoadingIndicator: false,
|
||||
cssClasses : {
|
||||
input: 'search-input'
|
||||
}
|
||||
}),
|
||||
|
||||
instantsearch.widgets.stats({
|
||||
container: '.algolia-stats',
|
||||
templates: {
|
||||
text: data => {
|
||||
const stats = CONFIG.i18n.hits_time
|
||||
.replace('${hits}', data.nbHits)
|
||||
.replace('${time}', data.processingTimeMS);
|
||||
return `<span>${stats}</span>
|
||||
<img src="${CONFIG.images}/logo-algolia-nebula-blue-full.svg" alt="Algolia">`;
|
||||
}
|
||||
},
|
||||
cssClasses: {
|
||||
text: 'search-stats'
|
||||
}
|
||||
}),
|
||||
|
||||
instantsearch.widgets.hits({
|
||||
container : '.algolia-hits',
|
||||
escapeHTML: false,
|
||||
templates : {
|
||||
item: data => {
|
||||
const { title, excerpt, excerptStrip, contentStripTruncate } = data._highlightResult;
|
||||
let result = `<a href="${data.permalink}" class="search-result-title">${title.value}</a>`;
|
||||
const content = excerpt || excerptStrip || contentStripTruncate;
|
||||
if (content && content.value) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = content.value;
|
||||
result += `<a href="${data.permalink}"><p class="search-result">${div.textContent.substring(0, 100)}...</p></a>`;
|
||||
if (data.nbHits === 0) {
|
||||
container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>';
|
||||
} else {
|
||||
const stats = CONFIG.i18n.hits_time
|
||||
.replace('${hits}', data.nbHits)
|
||||
.replace('${time}', Date.now() - startTime);
|
||||
let pagination = '';
|
||||
if (data.nbPages > 1) {
|
||||
pagination += '<nav class="pagination algolia-pagination">';
|
||||
for (let i = 0; i < data.nbPages; i++) {
|
||||
if (i === page) {
|
||||
pagination += `<span class="page-number current">${i + 1}</span>`;
|
||||
} else {
|
||||
pagination += `<a class="page-number" href="#" data-index=${i}>${i + 1}</a>`;
|
||||
}
|
||||
return result;
|
||||
},
|
||||
empty: data => {
|
||||
return `<div class="algolia-hits-empty">
|
||||
${CONFIG.i18n.empty.replace('${query}', data.query)}
|
||||
</div>`;
|
||||
}
|
||||
},
|
||||
cssClasses: {
|
||||
list: 'search-result-list'
|
||||
pagination += '</nav>';
|
||||
}
|
||||
}),
|
||||
|
||||
instantsearch.widgets.pagination({
|
||||
container: '.algolia-pagination',
|
||||
scrollTo : false,
|
||||
showFirst: false,
|
||||
showLast : false,
|
||||
templates: {
|
||||
first : '<i class="fa fa-angle-double-left"></i>',
|
||||
last : '<i class="fa fa-angle-double-right"></i>',
|
||||
previous: '<i class="fa fa-angle-left"></i>',
|
||||
next : '<i class="fa fa-angle-right"></i>'
|
||||
},
|
||||
cssClasses: {
|
||||
list : ['pagination', 'algolia-pagination'],
|
||||
item : 'pagination-item',
|
||||
link : 'page-number',
|
||||
selectedItem: 'current',
|
||||
disabledItem: 'disabled-item'
|
||||
}
|
||||
})
|
||||
]);
|
||||
container.innerHTML = `<div class="search-stats">
|
||||
<span>${stats}</span>
|
||||
<img src="${CONFIG.images}/logo-algolia-nebula-blue-full.svg" alt="Algolia">
|
||||
</div>
|
||||
<hr>
|
||||
<ul class="search-result-list">${data.hits.map(formatHits).join('')}</ul>
|
||||
${pagination}`;
|
||||
if (typeof pjax === 'object') pjax.refresh(container);
|
||||
container.querySelectorAll('.page-number').forEach(element => {
|
||||
element.addEventListener('click', async event => {
|
||||
event.preventDefault();
|
||||
await searchAlgolia(searchText, Number(element.dataset.index));
|
||||
});
|
||||
});
|
||||
}
|
||||
isSearching = false;
|
||||
if (pendingQuery !== null && (pendingQuery.searchText !== searchText || pendingQuery.page !== page)) {
|
||||
const { searchText, page } = pendingQuery;
|
||||
pendingQuery = null;
|
||||
searchAlgolia(searchText, page);
|
||||
}
|
||||
};
|
||||
|
||||
search.start();
|
||||
const inputEventFunction = async() => {
|
||||
const searchText = input.value.trim();
|
||||
if (searchText === '') {
|
||||
container.innerHTML = '<div class="search-result-icon"><i class="fab fa-algolia fa-5x"></i></div>';
|
||||
return;
|
||||
}
|
||||
// Algolia client will automatically cache the data for same queries
|
||||
await searchAlgolia(searchText, 0);
|
||||
};
|
||||
|
||||
const debouncedSearch = NexT.utils.debounce(inputEventFunction, 500);
|
||||
input.addEventListener('input', debouncedSearch);
|
||||
|
||||
// Handle and trigger popup window
|
||||
document.querySelectorAll('.popup-trigger').forEach(element => {
|
||||
element.addEventListener('click', () => {
|
||||
document.body.classList.add('search-active');
|
||||
setTimeout(() => document.querySelector('.search-input').focus(), 500);
|
||||
// Wait for search-popup animation to complete
|
||||
setTimeout(() => input.focus(), 500);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
5
source/js/third-party/search/local-search.js
vendored
5
source/js/third-party/search/local-search.js
vendored
@ -13,22 +13,20 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
const input = document.querySelector('.search-input');
|
||||
const container = document.querySelector('.search-result-container');
|
||||
|
||||
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) => {
|
||||
@ -41,7 +39,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
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>`;
|
||||
|
||||
@ -498,5 +498,14 @@ NexT.utils = {
|
||||
});
|
||||
intersectionObserver.observe(element);
|
||||
});
|
||||
},
|
||||
|
||||
debounce: function(func, wait) {
|
||||
let timeout;
|
||||
return function(...args) {
|
||||
const context = this;
|
||||
clearTimeout(timeout);
|
||||
timeout = setTimeout(() => func.apply(context, args), wait);
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user