Refactor Algolia search: drop instantsearch.js (#795)

This commit is contained in:
Mimi 2024-05-07 16:43:11 +08:00 committed by GitHub
parent 263841bd7a
commit 1ca10776ff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 141 additions and 195 deletions

View File

@ -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

View File

@ -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>

View File

@ -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 %}

View File

@ -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>

View File

@ -1,4 +1,3 @@
{{ next_vendors('algolia_search') }}
{{ next_vendors('instant_search') }}
{{- next_js('third-party/search/algolia-search.js') }}

View File

@ -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

View File

@ -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);
});
});

View File

@ -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>`;

View File

@ -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);
};
}
};