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 version: 4.23.3
file: dist/algoliasearch-lite.umd.js file: dist/algoliasearch-lite.umd.js
integrity: sha256-1QNshz86RqXe/qsCBldsUu13eAX6n/O98uubKQs87UI= 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: local_search:
name: hexo-generator-searchdb name: hexo-generator-searchdb
version: 1.4.1 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 %} {%- if theme.algolia_search.enable or theme.local_search.enable %}
<div class="search-pop-overlay"> <div class="search-pop-overlay">
<div class="popup search-popup"> <div class="popup search-popup">
{%- if theme.algolia_search.enable %} <div class="search-header">
{%- include 'algolia-search.njk' -%} <span class="search-icon">
{% elif theme.local_search.enable %} <i class="fa fa-search"></i>
{%- include 'localsearch.njk' -%} </span>
{%- endif %} <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>
</div> </div>
{%- endif %} {%- 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('algolia_search') }}
{{ next_vendors('instant_search') }}
{{- next_js('third-party/search/algolia-search.js') }} {{- 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 { .search-result-container {
display: flex;
flex-direction: column;
height: calc(100% - 55px); height: calc(100% - 55px);
overflow: auto; overflow: auto;
padding: 5px 25px; padding: 5px 25px;
hr { hr {
flex-shrink: 0;
margin: 5px 0 10px; margin: 5px 0 10px;
&:first-child { &:first-child {
@ -105,18 +108,27 @@ if (hexo-config('local_search.enable') or hexo-config('algolia_search.enable'))
border-bottom: 1px dashed $grey-light; border-bottom: 1px dashed $grey-light;
padding: 5px 0; 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')) { if (hexo-config('algolia_search.enable')) {
.search-input-container {
flex-grow: 1;
form {
padding: 2px;
}
}
.search-stats { .search-stats {
align-items: center; align-items: center;
display: flex; display: flex;
@ -128,52 +140,13 @@ if (hexo-config('algolia_search.enable')) {
} }
} }
.algolia-pagination { .pagination.algolia-pagination {
// Override default style of ul // Override the default style of pagination
margin: 40px 0; // Put pagination at the bottom when there is sufficient height
opacity: 1; margin-top: auto;
padding: 0; padding: 40px 0;
// Override motion
.pagination-item { // https://github.com/theme-next/hexo-theme-next/issues/537
display: inline-block; visibility: visible;
}
.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;
} }
} }

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', () => { document.addEventListener('DOMContentLoaded', () => {
const { indexName, appID, apiKey, hits } = CONFIG.algolia; const { indexName, appID, apiKey, hits } = CONFIG.algolia;
const client = algoliasearch(appID, apiKey);
const index = client.initIndex(indexName);
const search = instantsearch({ const input = document.querySelector('.search-input');
indexName, const container = document.querySelector('.search-result-container');
searchClient : algoliasearch(appID, apiKey),
searchFunction: helper => { const formatHits = data => {
if (document.querySelector('.search-input').value) { const { title, excerpt, excerptStrip, contentStripTruncate } = data._highlightResult;
helper.search(); 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') { let isSearching = false;
search.on('render', () => { let pendingQuery = null;
pjax.refresh(document.querySelector('.algolia-hits'));
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>'
}); });
} if (data.nbHits === 0) {
container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>';
// Registering Widgets } else {
search.addWidgets([ const stats = CONFIG.i18n.hits_time
instantsearch.widgets.configure({ .replace('${hits}', data.nbHits)
hitsPerPage: hits.per_page || 10 .replace('${time}', Date.now() - startTime);
}), let pagination = '';
if (data.nbPages > 1) {
instantsearch.widgets.searchBox({ pagination += '<nav class="pagination algolia-pagination">';
container : '.search-input-container', for (let i = 0; i < data.nbPages; i++) {
placeholder : CONFIG.i18n.placeholder, if (i === page) {
// Hide default icons of algolia search pagination += `<span class="page-number current">${i + 1}</span>`;
showReset : false, } else {
showSubmit : false, pagination += `<a class="page-number" href="#" data-index=${i}>${i + 1}</a>`;
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>`;
} }
return result;
},
empty: data => {
return `<div class="algolia-hits-empty">
${CONFIG.i18n.empty.replace('${query}', data.query)}
</div>`;
} }
}, pagination += '</nav>';
cssClasses: {
list: 'search-result-list'
} }
}),
instantsearch.widgets.pagination({ container.innerHTML = `<div class="search-stats">
container: '.algolia-pagination', <span>${stats}</span>
scrollTo : false, <img src="${CONFIG.images}/logo-algolia-nebula-blue-full.svg" alt="Algolia">
showFirst: false, </div>
showLast : false, <hr>
templates: { <ul class="search-result-list">${data.hits.map(formatHits).join('')}</ul>
first : '<i class="fa fa-angle-double-left"></i>', ${pagination}`;
last : '<i class="fa fa-angle-double-right"></i>', if (typeof pjax === 'object') pjax.refresh(container);
previous: '<i class="fa fa-angle-left"></i>', container.querySelectorAll('.page-number').forEach(element => {
next : '<i class="fa fa-angle-right"></i>' element.addEventListener('click', async event => {
}, event.preventDefault();
cssClasses: { await searchAlgolia(searchText, Number(element.dataset.index));
list : ['pagination', 'algolia-pagination'], });
item : 'pagination-item', });
link : 'page-number', }
selectedItem: 'current', isSearching = false;
disabledItem: 'disabled-item' 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 // Handle and trigger popup window
document.querySelectorAll('.popup-trigger').forEach(element => { document.querySelectorAll('.popup-trigger').forEach(element => {
element.addEventListener('click', () => { element.addEventListener('click', () => {
document.body.classList.add('search-active'); 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 input = document.querySelector('.search-input');
const container = document.querySelector('.search-result-container');
const inputEventFunction = () => { const inputEventFunction = () => {
if (!localSearch.isfetched) return; if (!localSearch.isfetched) return;
const searchText = input.value.trim().toLowerCase(); const searchText = input.value.trim().toLowerCase();
const keywords = searchText.split(/[-\s]+/); const keywords = searchText.split(/[-\s]+/);
const container = document.querySelector('.search-result-container');
let resultItems = []; let resultItems = [];
if (searchText.length > 0) { if (searchText.length > 0) {
// Perform local searching // Perform local searching
resultItems = localSearch.getResultItems(keywords); resultItems = localSearch.getResultItems(keywords);
} }
if (keywords.length === 1 && keywords[0] === '') { 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>'; container.innerHTML = '<div class="search-result-icon"><i class="fa fa-search fa-5x"></i></div>';
} else if (resultItems.length === 0) { } 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>'; container.innerHTML = '<div class="search-result-icon"><i class="far fa-frown fa-5x"></i></div>';
} else { } else {
resultItems.sort((left, right) => { resultItems.sort((left, right) => {
@ -41,7 +39,6 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
const stats = CONFIG.i18n.hits.replace('${hits}', resultItems.length); const stats = CONFIG.i18n.hits.replace('${hits}', resultItems.length);
container.classList.remove('no-result');
container.innerHTML = `<div class="search-stats">${stats}</div> container.innerHTML = `<div class="search-stats">${stats}</div>
<hr> <hr>
<ul class="search-result-list">${resultItems.map(result => result.item).join('')}</ul>`; <ul class="search-result-list">${resultItems.map(result => result.item).join('')}</ul>`;

View File

@ -498,5 +498,14 @@ NexT.utils = {
}); });
intersectionObserver.observe(element); intersectionObserver.observe(element);
}); });
},
debounce: function(func, wait) {
let timeout;
return function(...args) {
const context = this;
clearTimeout(timeout);
timeout = setTimeout(() => func.apply(context, args), wait);
};
} }
}; };