mirror of
https://github.com/immich-app/immich.git
synced 2025-05-31 04:05:39 -04:00
fix(docs): search (#6605)
This commit is contained in:
parent
61bb52ac11
commit
d801131f38
@ -1,256 +0,0 @@
|
|||||||
import Hogan from 'hogan.js';
|
|
||||||
import LunrSearchAdapter from './lunar-search';
|
|
||||||
import autocomplete from 'autocomplete.js';
|
|
||||||
import templates from './templates';
|
|
||||||
import utils from './utils';
|
|
||||||
import $ from 'autocomplete.js/zepto';
|
|
||||||
|
|
||||||
class DocSearch {
|
|
||||||
constructor({
|
|
||||||
searchDocs,
|
|
||||||
searchIndex,
|
|
||||||
inputSelector,
|
|
||||||
debug = false,
|
|
||||||
baseUrl = '/',
|
|
||||||
queryDataCallback = null,
|
|
||||||
autocompleteOptions = {
|
|
||||||
debug: false,
|
|
||||||
hint: false,
|
|
||||||
autoselect: true,
|
|
||||||
},
|
|
||||||
transformData = false,
|
|
||||||
queryHook = false,
|
|
||||||
handleSelected = false,
|
|
||||||
enhancedSearchInput = false,
|
|
||||||
layout = 'collumns',
|
|
||||||
}) {
|
|
||||||
this.input = DocSearch.getInputFromSelector(inputSelector);
|
|
||||||
this.queryDataCallback = queryDataCallback || null;
|
|
||||||
const autocompleteOptionsDebug =
|
|
||||||
autocompleteOptions && autocompleteOptions.debug ? autocompleteOptions.debug : false;
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
autocompleteOptions.debug = debug || autocompleteOptionsDebug;
|
|
||||||
this.autocompleteOptions = autocompleteOptions;
|
|
||||||
this.autocompleteOptions.cssClasses = this.autocompleteOptions.cssClasses || {};
|
|
||||||
this.autocompleteOptions.cssClasses.prefix = this.autocompleteOptions.cssClasses.prefix || 'ds';
|
|
||||||
const inputAriaLabel = this.input && typeof this.input.attr === 'function' && this.input.attr('aria-label');
|
|
||||||
this.autocompleteOptions.ariaLabel = this.autocompleteOptions.ariaLabel || inputAriaLabel || 'search input';
|
|
||||||
|
|
||||||
this.isSimpleLayout = layout === 'simple';
|
|
||||||
|
|
||||||
this.client = new LunrSearchAdapter(searchDocs, searchIndex, baseUrl);
|
|
||||||
|
|
||||||
if (enhancedSearchInput) {
|
|
||||||
this.input = DocSearch.injectSearchBox(this.input);
|
|
||||||
}
|
|
||||||
this.autocomplete = autocomplete(this.input, autocompleteOptions, [
|
|
||||||
{
|
|
||||||
source: this.getAutocompleteSource(transformData, queryHook),
|
|
||||||
templates: {
|
|
||||||
suggestion: DocSearch.getSuggestionTemplate(this.isSimpleLayout),
|
|
||||||
footer: templates.footer,
|
|
||||||
empty: DocSearch.getEmptyTemplate(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const customHandleSelected = handleSelected;
|
|
||||||
this.handleSelected = customHandleSelected || this.handleSelected;
|
|
||||||
|
|
||||||
// We prevent default link clicking if a custom handleSelected is defined
|
|
||||||
if (customHandleSelected) {
|
|
||||||
$('.algolia-autocomplete').on('click', '.ds-suggestions a', (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.autocomplete.on('autocomplete:selected', this.handleSelected.bind(null, this.autocomplete.autocomplete));
|
|
||||||
|
|
||||||
this.autocomplete.on('autocomplete:shown', this.handleShown.bind(null, this.input));
|
|
||||||
|
|
||||||
if (enhancedSearchInput) {
|
|
||||||
DocSearch.bindSearchBoxEvent();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static injectSearchBox(input) {
|
|
||||||
input.before(templates.searchBox);
|
|
||||||
const newInput = input.prev().prev().find('input');
|
|
||||||
input.remove();
|
|
||||||
return newInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
static bindSearchBoxEvent() {
|
|
||||||
$('.searchbox [type="reset"]').on('click', function () {
|
|
||||||
$('input#docsearch').focus();
|
|
||||||
$(this).addClass('hide');
|
|
||||||
autocomplete.autocomplete.setVal('');
|
|
||||||
});
|
|
||||||
|
|
||||||
$('input#docsearch').on('keyup', () => {
|
|
||||||
const searchbox = document.querySelector('input#docsearch');
|
|
||||||
const reset = document.querySelector('.searchbox [type="reset"]');
|
|
||||||
reset.className = 'searchbox__reset';
|
|
||||||
if (searchbox.value.length === 0) {
|
|
||||||
reset.className += ' hide';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the matching input from a CSS selector, null if none matches
|
|
||||||
* @function getInputFromSelector
|
|
||||||
* @param {string} selector CSS selector that matches the search
|
|
||||||
* input of the page
|
|
||||||
* @returns {void}
|
|
||||||
*/
|
|
||||||
static getInputFromSelector(selector) {
|
|
||||||
const input = $(selector).filter('input');
|
|
||||||
return input.length ? $(input[0]) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the `source` method to be passed to autocomplete.js. It will query
|
|
||||||
* the Algolia index and call the callbacks with the formatted hits.
|
|
||||||
* @function getAutocompleteSource
|
|
||||||
* @param {function} transformData An optional function to transform the hits
|
|
||||||
* @param {function} queryHook An optional function to transform the query
|
|
||||||
* @returns {function} Method to be passed as the `source` option of
|
|
||||||
* autocomplete
|
|
||||||
*/
|
|
||||||
getAutocompleteSource(transformData, queryHook) {
|
|
||||||
return (query, callback) => {
|
|
||||||
if (queryHook) {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
query = queryHook(query) || query;
|
|
||||||
}
|
|
||||||
this.client.search(query).then((hits) => {
|
|
||||||
if (this.queryDataCallback && typeof this.queryDataCallback == 'function') {
|
|
||||||
this.queryDataCallback(hits);
|
|
||||||
}
|
|
||||||
if (transformData) {
|
|
||||||
hits = transformData(hits) || hits;
|
|
||||||
}
|
|
||||||
callback(DocSearch.formatHits(hits));
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Given a list of hits returned by the API, will reformat them to be used in
|
|
||||||
// a Hogan template
|
|
||||||
static formatHits(receivedHits) {
|
|
||||||
const clonedHits = utils.deepClone(receivedHits);
|
|
||||||
const hits = clonedHits.map((hit) => {
|
|
||||||
if (hit._highlightResult) {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
hit._highlightResult = utils.mergeKeyWithParent(hit._highlightResult, 'hierarchy');
|
|
||||||
}
|
|
||||||
return utils.mergeKeyWithParent(hit, 'hierarchy');
|
|
||||||
});
|
|
||||||
|
|
||||||
// Group hits by category / subcategory
|
|
||||||
let groupedHits = utils.groupBy(hits, 'lvl0');
|
|
||||||
$.each(groupedHits, (level, collection) => {
|
|
||||||
const groupedHitsByLvl1 = utils.groupBy(collection, 'lvl1');
|
|
||||||
const flattenedHits = utils.flattenAndFlagFirst(groupedHitsByLvl1, 'isSubCategoryHeader');
|
|
||||||
groupedHits[level] = flattenedHits;
|
|
||||||
});
|
|
||||||
groupedHits = utils.flattenAndFlagFirst(groupedHits, 'isCategoryHeader');
|
|
||||||
|
|
||||||
// Translate hits into smaller objects to be send to the template
|
|
||||||
return groupedHits.map((hit) => {
|
|
||||||
const url = DocSearch.formatURL(hit);
|
|
||||||
const category = utils.getHighlightedValue(hit, 'lvl0');
|
|
||||||
const subcategory = utils.getHighlightedValue(hit, 'lvl1') || category;
|
|
||||||
const displayTitle = utils
|
|
||||||
.compact([
|
|
||||||
utils.getHighlightedValue(hit, 'lvl2') || subcategory,
|
|
||||||
utils.getHighlightedValue(hit, 'lvl3'),
|
|
||||||
utils.getHighlightedValue(hit, 'lvl4'),
|
|
||||||
utils.getHighlightedValue(hit, 'lvl5'),
|
|
||||||
utils.getHighlightedValue(hit, 'lvl6'),
|
|
||||||
])
|
|
||||||
.join('<span class="aa-suggestion-title-separator" aria-hidden="true"> › </span>');
|
|
||||||
const text = utils.getSnippetedValue(hit, 'content');
|
|
||||||
const isTextOrSubcategoryNonEmpty = (subcategory && subcategory !== '') || (displayTitle && displayTitle !== '');
|
|
||||||
const isLvl1EmptyOrDuplicate = !subcategory || subcategory === '' || subcategory === category;
|
|
||||||
const isLvl2 = displayTitle && displayTitle !== '' && displayTitle !== subcategory;
|
|
||||||
const isLvl1 = !isLvl2 && subcategory && subcategory !== '' && subcategory !== category;
|
|
||||||
const isLvl0 = !isLvl1 && !isLvl2;
|
|
||||||
|
|
||||||
return {
|
|
||||||
isLvl0,
|
|
||||||
isLvl1,
|
|
||||||
isLvl2,
|
|
||||||
isLvl1EmptyOrDuplicate,
|
|
||||||
isCategoryHeader: hit.isCategoryHeader,
|
|
||||||
isSubCategoryHeader: hit.isSubCategoryHeader,
|
|
||||||
isTextOrSubcategoryNonEmpty,
|
|
||||||
category,
|
|
||||||
subcategory,
|
|
||||||
title: displayTitle,
|
|
||||||
text,
|
|
||||||
url,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
static formatURL(hit) {
|
|
||||||
const { url, anchor } = hit;
|
|
||||||
if (url) {
|
|
||||||
const containsAnchor = url.indexOf('#') !== -1;
|
|
||||||
if (containsAnchor) return url;
|
|
||||||
else if (anchor) return `${hit.url}#${hit.anchor}`;
|
|
||||||
return url;
|
|
||||||
} else if (anchor) return `#${hit.anchor}`;
|
|
||||||
/* eslint-disable */
|
|
||||||
console.warn('no anchor nor url for : ', JSON.stringify(hit));
|
|
||||||
/* eslint-enable */
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
static getEmptyTemplate() {
|
|
||||||
return (args) => Hogan.compile(templates.empty).render(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
static getSuggestionTemplate(isSimpleLayout) {
|
|
||||||
const stringTemplate = isSimpleLayout ? templates.suggestionSimple : templates.suggestion;
|
|
||||||
const template = Hogan.compile(stringTemplate);
|
|
||||||
return (suggestion) => template.render(suggestion);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleSelected(input, event, suggestion, datasetNumber, context = {}) {
|
|
||||||
// Do nothing if click on the suggestion, as it's already a <a href>, the
|
|
||||||
// browser will take care of it. This allow Ctrl-Clicking on results and not
|
|
||||||
// having the main window being redirected as well
|
|
||||||
if (context.selectionMethod === 'click') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
input.setVal('');
|
|
||||||
window.location.assign(suggestion.url);
|
|
||||||
}
|
|
||||||
|
|
||||||
handleShown(input) {
|
|
||||||
const middleOfInput = input.offset().left + input.width() / 2;
|
|
||||||
let middleOfWindow = $(document).width() / 2;
|
|
||||||
|
|
||||||
if (isNaN(middleOfWindow)) {
|
|
||||||
middleOfWindow = 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alignClass = middleOfInput - middleOfWindow >= 0 ? 'algolia-autocomplete-right' : 'algolia-autocomplete-left';
|
|
||||||
const otherAlignClass =
|
|
||||||
middleOfInput - middleOfWindow < 0 ? 'algolia-autocomplete-right' : 'algolia-autocomplete-left';
|
|
||||||
const autocompleteWrapper = $('.algolia-autocomplete');
|
|
||||||
if (!autocompleteWrapper.hasClass(alignClass)) {
|
|
||||||
autocompleteWrapper.addClass(alignClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (autocompleteWrapper.hasClass(otherAlignClass)) {
|
|
||||||
autocompleteWrapper.removeClass(otherAlignClass);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DocSearch;
|
|
File diff suppressed because one or more lines are too long
@ -1,111 +0,0 @@
|
|||||||
import React, { useRef, useCallback, useState } from 'react';
|
|
||||||
import classnames from 'classnames';
|
|
||||||
import { useHistory } from '@docusaurus/router';
|
|
||||||
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
|
|
||||||
import { usePluginData } from '@docusaurus/useGlobalData';
|
|
||||||
import useIsBrowser from '@docusaurus/useIsBrowser';
|
|
||||||
const Search = (props) => {
|
|
||||||
const initialized = useRef(false);
|
|
||||||
const searchBarRef = useRef(null);
|
|
||||||
const [indexReady, setIndexReady] = useState(false);
|
|
||||||
const history = useHistory();
|
|
||||||
const { siteConfig = {} } = useDocusaurusContext();
|
|
||||||
const isBrowser = useIsBrowser();
|
|
||||||
const { baseUrl } = siteConfig;
|
|
||||||
const initAlgolia = (searchDocs, searchIndex, DocSearch) => {
|
|
||||||
new DocSearch({
|
|
||||||
searchDocs,
|
|
||||||
searchIndex,
|
|
||||||
baseUrl,
|
|
||||||
inputSelector: '#search_input_react',
|
|
||||||
// Override algolia's default selection event, allowing us to do client-side
|
|
||||||
// navigation and avoiding a full page refresh.
|
|
||||||
handleSelected: (_input, _event, suggestion) => {
|
|
||||||
const url = suggestion.url || '/';
|
|
||||||
// Use an anchor tag to parse the absolute url into a relative url
|
|
||||||
// Alternatively, we can use new URL(suggestion.url) but its not supported in IE
|
|
||||||
const a = document.createElement('a');
|
|
||||||
a.href = url;
|
|
||||||
// Algolia use closest parent element id #__docusaurus when a h1 page title does not have an id
|
|
||||||
// So, we can safely remove it. See https://github.com/facebook/docusaurus/issues/1828 for more details.
|
|
||||||
|
|
||||||
history.push(url);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const pluginData = usePluginData('docusaurus-lunr-search');
|
|
||||||
const getSearchDoc = () =>
|
|
||||||
process.env.NODE_ENV === 'production'
|
|
||||||
? fetch(`${baseUrl}${pluginData.fileNames.searchDoc}`).then((content) => content.json())
|
|
||||||
: Promise.resolve([]);
|
|
||||||
|
|
||||||
const getLunrIndex = () =>
|
|
||||||
process.env.NODE_ENV === 'production'
|
|
||||||
? fetch(`${baseUrl}${pluginData.fileNames.lunrIndex}`).then((content) => content.json())
|
|
||||||
: Promise.resolve([]);
|
|
||||||
|
|
||||||
const loadAlgolia = () => {
|
|
||||||
if (!initialized.current) {
|
|
||||||
Promise.all([getSearchDoc(), getLunrIndex(), import('./DocSearch'), import('./algolia.css')]).then(
|
|
||||||
([searchDocs, searchIndex, { default: DocSearch }]) => {
|
|
||||||
if (searchDocs.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
initAlgolia(searchDocs, searchIndex, DocSearch);
|
|
||||||
setIndexReady(true);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
initialized.current = true;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const toggleSearchIconClick = useCallback(
|
|
||||||
(e) => {
|
|
||||||
if (!searchBarRef.current.contains(e.target)) {
|
|
||||||
searchBarRef.current.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
props.handleSearchBarToggle && props.handleSearchBarToggle(!props.isSearchBarExpanded);
|
|
||||||
},
|
|
||||||
[props.isSearchBarExpanded],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (isBrowser) {
|
|
||||||
loadAlgolia();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="navbar__search" key="search-box">
|
|
||||||
<span
|
|
||||||
aria-label="expand searchbar"
|
|
||||||
role="button"
|
|
||||||
className={classnames('search-icon', {
|
|
||||||
'search-icon-hidden': props.isSearchBarExpanded,
|
|
||||||
})}
|
|
||||||
onClick={toggleSearchIconClick}
|
|
||||||
onKeyDown={toggleSearchIconClick}
|
|
||||||
tabIndex={0}
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
id="search_input_react"
|
|
||||||
type="search"
|
|
||||||
placeholder={indexReady ? 'Search' : 'Loading...'}
|
|
||||||
aria-label="Search"
|
|
||||||
className={classnames(
|
|
||||||
'navbar__search-input',
|
|
||||||
{ 'search-bar-expanded': props.isSearchBarExpanded },
|
|
||||||
{ 'search-bar': !props.isSearchBarExpanded },
|
|
||||||
)}
|
|
||||||
onClick={loadAlgolia}
|
|
||||||
onMouseOver={loadAlgolia}
|
|
||||||
onFocus={toggleSearchIconClick}
|
|
||||||
onBlur={toggleSearchIconClick}
|
|
||||||
ref={searchBarRef}
|
|
||||||
disabled={!indexReady}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Search;
|
|
@ -1,161 +0,0 @@
|
|||||||
import lunr from '@generated/lunr.client';
|
|
||||||
lunr.tokenizer.separator = /[\s\-/]+/;
|
|
||||||
|
|
||||||
class LunrSearchAdapter {
|
|
||||||
constructor(searchDocs, searchIndex, baseUrl = '/') {
|
|
||||||
this.searchDocs = searchDocs;
|
|
||||||
this.lunrIndex = lunr.Index.load(searchIndex);
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
}
|
|
||||||
|
|
||||||
getLunrResult(input) {
|
|
||||||
return this.lunrIndex.query(function (query) {
|
|
||||||
const tokens = lunr.tokenizer(input);
|
|
||||||
query.term(tokens, {
|
|
||||||
boost: 10,
|
|
||||||
});
|
|
||||||
query.term(tokens, {
|
|
||||||
wildcard: lunr.Query.wildcard.TRAILING,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
getHit(doc, formattedTitle, formattedContent) {
|
|
||||||
return {
|
|
||||||
hierarchy: {
|
|
||||||
lvl0: doc.pageTitle || doc.title,
|
|
||||||
lvl1: doc.type === 0 ? null : doc.title,
|
|
||||||
},
|
|
||||||
url: doc.url,
|
|
||||||
_snippetResult: formattedContent
|
|
||||||
? {
|
|
||||||
content: {
|
|
||||||
value: formattedContent,
|
|
||||||
matchLevel: 'full',
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: null,
|
|
||||||
_highlightResult: {
|
|
||||||
hierarchy: {
|
|
||||||
lvl0: {
|
|
||||||
value: doc.type === 0 ? formattedTitle || doc.title : doc.pageTitle,
|
|
||||||
},
|
|
||||||
lvl1:
|
|
||||||
doc.type === 0
|
|
||||||
? null
|
|
||||||
: {
|
|
||||||
value: formattedTitle || doc.title,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
getTitleHit(doc, position, length) {
|
|
||||||
const start = position[0];
|
|
||||||
const end = position[0] + length;
|
|
||||||
let formattedTitle =
|
|
||||||
doc.title.substring(0, start) +
|
|
||||||
'<span class="algolia-docsearch-suggestion--highlight">' +
|
|
||||||
doc.title.substring(start, end) +
|
|
||||||
'</span>' +
|
|
||||||
doc.title.substring(end, doc.title.length);
|
|
||||||
return this.getHit(doc, formattedTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
getKeywordHit(doc, position, length) {
|
|
||||||
const start = position[0];
|
|
||||||
const end = position[0] + length;
|
|
||||||
let formattedTitle =
|
|
||||||
doc.title +
|
|
||||||
'<br /><i>Keywords: ' +
|
|
||||||
doc.keywords.substring(0, start) +
|
|
||||||
'<span class="algolia-docsearch-suggestion--highlight">' +
|
|
||||||
doc.keywords.substring(start, end) +
|
|
||||||
'</span>' +
|
|
||||||
doc.keywords.substring(end, doc.keywords.length) +
|
|
||||||
'</i>';
|
|
||||||
return this.getHit(doc, formattedTitle);
|
|
||||||
}
|
|
||||||
|
|
||||||
getContentHit(doc, position) {
|
|
||||||
const start = position[0];
|
|
||||||
const end = position[0] + position[1];
|
|
||||||
let previewStart = start;
|
|
||||||
let previewEnd = end;
|
|
||||||
let ellipsesBefore = true;
|
|
||||||
let ellipsesAfter = true;
|
|
||||||
for (let k = 0; k < 3; k++) {
|
|
||||||
const nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
|
|
||||||
const nextDot = doc.content.lastIndexOf('.', previewStart - 2);
|
|
||||||
if (nextDot > 0 && nextDot > nextSpace) {
|
|
||||||
previewStart = nextDot + 1;
|
|
||||||
ellipsesBefore = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (nextSpace < 0) {
|
|
||||||
previewStart = 0;
|
|
||||||
ellipsesBefore = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
previewStart = nextSpace + 1;
|
|
||||||
}
|
|
||||||
for (let k = 0; k < 10; k++) {
|
|
||||||
const nextSpace = doc.content.indexOf(' ', previewEnd + 1);
|
|
||||||
const nextDot = doc.content.indexOf('.', previewEnd + 1);
|
|
||||||
if (nextDot > 0 && nextDot < nextSpace) {
|
|
||||||
previewEnd = nextDot;
|
|
||||||
ellipsesAfter = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (nextSpace < 0) {
|
|
||||||
previewEnd = doc.content.length;
|
|
||||||
ellipsesAfter = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
previewEnd = nextSpace;
|
|
||||||
}
|
|
||||||
let preview = doc.content.substring(previewStart, start);
|
|
||||||
if (ellipsesBefore) {
|
|
||||||
preview = '... ' + preview;
|
|
||||||
}
|
|
||||||
preview += '<span class="algolia-docsearch-suggestion--highlight">' + doc.content.substring(start, end) + '</span>';
|
|
||||||
preview += doc.content.substring(end, previewEnd);
|
|
||||||
if (ellipsesAfter) {
|
|
||||||
preview += ' ...';
|
|
||||||
}
|
|
||||||
return this.getHit(doc, null, preview);
|
|
||||||
}
|
|
||||||
search(input) {
|
|
||||||
return new Promise((resolve, rej) => {
|
|
||||||
const results = this.getLunrResult(input);
|
|
||||||
const hits = [];
|
|
||||||
results.length > 5 && (results.length = 5);
|
|
||||||
this.titleHitsRes = [];
|
|
||||||
this.contentHitsRes = [];
|
|
||||||
results.forEach((result) => {
|
|
||||||
const doc = this.searchDocs[result.ref];
|
|
||||||
const { metadata } = result.matchData;
|
|
||||||
for (let i in metadata) {
|
|
||||||
if (metadata[i].title) {
|
|
||||||
if (!this.titleHitsRes.includes(result.ref)) {
|
|
||||||
const position = metadata[i].title.position[0];
|
|
||||||
hits.push(this.getTitleHit(doc, position, input.length));
|
|
||||||
this.titleHitsRes.push(result.ref);
|
|
||||||
}
|
|
||||||
} else if (metadata[i].content) {
|
|
||||||
const position = metadata[i].content.position[0];
|
|
||||||
hits.push(this.getContentHit(doc, position));
|
|
||||||
} else if (metadata[i].keywords) {
|
|
||||||
const position = metadata[i].keywords.position[0];
|
|
||||||
hits.push(this.getKeywordHit(doc, position, input.length));
|
|
||||||
this.titleHitsRes.push(result.ref);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
hits.length > 5 && (hits.length = 5);
|
|
||||||
resolve(hits);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default LunrSearchAdapter;
|
|
@ -1,33 +0,0 @@
|
|||||||
.search-icon {
|
|
||||||
background-image: var(--ifm-navbar-search-input-icon);
|
|
||||||
height: auto;
|
|
||||||
width: 24px;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 8px;
|
|
||||||
line-height: 32px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-position: center;
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon-hidden {
|
|
||||||
visibility: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 360px) {
|
|
||||||
.search-bar {
|
|
||||||
width: 0 !important;
|
|
||||||
background: none !important;
|
|
||||||
padding: 0 !important;
|
|
||||||
transition: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-bar-expanded {
|
|
||||||
width: 9rem !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
display: inline;
|
|
||||||
vertical-align: sub;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,112 +0,0 @@
|
|||||||
const prefix = 'algolia-docsearch';
|
|
||||||
const suggestionPrefix = `${prefix}-suggestion`;
|
|
||||||
const footerPrefix = `${prefix}-footer`;
|
|
||||||
|
|
||||||
const templates = {
|
|
||||||
suggestion: `
|
|
||||||
<a class="${suggestionPrefix}
|
|
||||||
{{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
|
|
||||||
{{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
|
|
||||||
"
|
|
||||||
aria-label="Link to the result"
|
|
||||||
href="{{{url}}}"
|
|
||||||
>
|
|
||||||
<div class="${suggestionPrefix}--category-header">
|
|
||||||
<span class="${suggestionPrefix}--category-header-lvl0">{{{category}}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="${suggestionPrefix}--wrapper">
|
|
||||||
<div class="${suggestionPrefix}--subcategory-column">
|
|
||||||
<span class="${suggestionPrefix}--subcategory-column-text">{{{subcategory}}}</span>
|
|
||||||
</div>
|
|
||||||
{{#isTextOrSubcategoryNonEmpty}}
|
|
||||||
<div class="${suggestionPrefix}--content">
|
|
||||||
<div class="${suggestionPrefix}--subcategory-inline">{{{subcategory}}}</div>
|
|
||||||
<div class="${suggestionPrefix}--title">{{{title}}}</div>
|
|
||||||
{{#text}}<div class="${suggestionPrefix}--text">{{{text}}}</div>{{/text}}
|
|
||||||
</div>
|
|
||||||
{{/isTextOrSubcategoryNonEmpty}}
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
`,
|
|
||||||
suggestionSimple: `
|
|
||||||
<div class="${suggestionPrefix}
|
|
||||||
{{#isCategoryHeader}}${suggestionPrefix}__main{{/isCategoryHeader}}
|
|
||||||
{{#isSubCategoryHeader}}${suggestionPrefix}__secondary{{/isSubCategoryHeader}}
|
|
||||||
suggestion-layout-simple
|
|
||||||
">
|
|
||||||
<div class="${suggestionPrefix}--category-header">
|
|
||||||
{{^isLvl0}}
|
|
||||||
<span class="${suggestionPrefix}--category-header-lvl0 ${suggestionPrefix}--category-header-item">{{{category}}}</span>
|
|
||||||
{{^isLvl1}}
|
|
||||||
{{^isLvl1EmptyOrDuplicate}}
|
|
||||||
<span class="${suggestionPrefix}--category-header-lvl1 ${suggestionPrefix}--category-header-item">
|
|
||||||
{{{subcategory}}}
|
|
||||||
</span>
|
|
||||||
{{/isLvl1EmptyOrDuplicate}}
|
|
||||||
{{/isLvl1}}
|
|
||||||
{{/isLvl0}}
|
|
||||||
<div class="${suggestionPrefix}--title ${suggestionPrefix}--category-header-item">
|
|
||||||
{{#isLvl2}}
|
|
||||||
{{{title}}}
|
|
||||||
{{/isLvl2}}
|
|
||||||
{{#isLvl1}}
|
|
||||||
{{{subcategory}}}
|
|
||||||
{{/isLvl1}}
|
|
||||||
{{#isLvl0}}
|
|
||||||
{{{category}}}
|
|
||||||
{{/isLvl0}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="${suggestionPrefix}--wrapper">
|
|
||||||
{{#text}}
|
|
||||||
<div class="${suggestionPrefix}--content">
|
|
||||||
<div class="${suggestionPrefix}--text">{{{text}}}</div>
|
|
||||||
</div>
|
|
||||||
{{/text}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
footer: `
|
|
||||||
<div class="${footerPrefix}">
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
empty: `
|
|
||||||
<div class="${suggestionPrefix}">
|
|
||||||
<div class="${suggestionPrefix}--wrapper">
|
|
||||||
<div class="${suggestionPrefix}--content ${suggestionPrefix}--no-results">
|
|
||||||
<div class="${suggestionPrefix}--title">
|
|
||||||
<div class="${suggestionPrefix}--text">
|
|
||||||
No results found for query <b>"{{query}}"</b>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
searchBox: `
|
|
||||||
<form novalidate="novalidate" onsubmit="return false;" class="searchbox">
|
|
||||||
<div role="search" class="searchbox__wrapper">
|
|
||||||
<input id="docsearch" type="search" name="search" placeholder="Search the docs" autocomplete="off" required="required" class="searchbox__input"/>
|
|
||||||
<button type="submit" title="Submit your search query." class="searchbox__submit" >
|
|
||||||
<svg width=12 height=12 role="img" aria-label="Search">
|
|
||||||
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-search-13"></use>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<button type="reset" title="Clear the search query." class="searchbox__reset hide">
|
|
||||||
<svg width=12 height=12 role="img" aria-label="Reset">
|
|
||||||
<use xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="#sbx-icon-clear-3"></use>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
<div class="svg-icons" style="height: 0; width: 0; position: absolute; visibility: hidden">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<symbol id="sbx-icon-clear-3" viewBox="0 0 40 40"><path d="M16.228 20L1.886 5.657 0 3.772 3.772 0l1.885 1.886L20 16.228 34.343 1.886 36.228 0 40 3.772l-1.886 1.885L23.772 20l14.342 14.343L40 36.228 36.228 40l-1.885-1.886L20 23.772 5.657 38.114 3.772 40 0 36.228l1.886-1.885L16.228 20z" fill-rule="evenodd"></symbol>
|
|
||||||
<symbol id="sbx-icon-search-13" viewBox="0 0 40 40"><path d="M26.806 29.012a16.312 16.312 0 0 1-10.427 3.746C7.332 32.758 0 25.425 0 16.378 0 7.334 7.333 0 16.38 0c9.045 0 16.378 7.333 16.378 16.38 0 3.96-1.406 7.593-3.746 10.426L39.547 37.34c.607.608.61 1.59-.004 2.203a1.56 1.56 0 0 1-2.202.004L26.807 29.012zm-10.427.627c7.322 0 13.26-5.938 13.26-13.26 0-7.324-5.938-13.26-13.26-13.26-7.324 0-13.26 5.936-13.26 13.26 0 7.322 5.936 13.26 13.26 13.26z" fill-rule="evenodd"></symbol>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
`,
|
|
||||||
};
|
|
||||||
|
|
||||||
export default templates;
|
|
@ -1,266 +0,0 @@
|
|||||||
import $ from 'autocomplete.js/zepto';
|
|
||||||
|
|
||||||
const utils = {
|
|
||||||
/*
|
|
||||||
* Move the content of an object key one level higher.
|
|
||||||
* eg.
|
|
||||||
* {
|
|
||||||
* name: 'My name',
|
|
||||||
* hierarchy: {
|
|
||||||
* lvl0: 'Foo',
|
|
||||||
* lvl1: 'Bar'
|
|
||||||
* }
|
|
||||||
* }
|
|
||||||
* Will be converted to
|
|
||||||
* {
|
|
||||||
* name: 'My name',
|
|
||||||
* lvl0: 'Foo',
|
|
||||||
* lvl1: 'Bar'
|
|
||||||
* }
|
|
||||||
* @param {Object} object Main object
|
|
||||||
* @param {String} property Main object key to move up
|
|
||||||
* @return {Object}
|
|
||||||
* @throws Error when key is not an attribute of Object or is not an object itself
|
|
||||||
*/
|
|
||||||
mergeKeyWithParent(object, property) {
|
|
||||||
if (object[property] === undefined) {
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
if (typeof object[property] !== 'object') {
|
|
||||||
return object;
|
|
||||||
}
|
|
||||||
const newObject = $.extend({}, object, object[property]);
|
|
||||||
delete newObject[property];
|
|
||||||
return newObject;
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
* Group all objects of a collection by the value of the specified attribute
|
|
||||||
* If the attribute is a string, use the lowercase form.
|
|
||||||
*
|
|
||||||
* eg.
|
|
||||||
* groupBy([
|
|
||||||
* {name: 'Tim', category: 'dev'},
|
|
||||||
* {name: 'Vincent', category: 'dev'},
|
|
||||||
* {name: 'Ben', category: 'sales'},
|
|
||||||
* {name: 'Jeremy', category: 'sales'},
|
|
||||||
* {name: 'AlexS', category: 'dev'},
|
|
||||||
* {name: 'AlexK', category: 'sales'}
|
|
||||||
* ], 'category');
|
|
||||||
* =>
|
|
||||||
* {
|
|
||||||
* 'devs': [
|
|
||||||
* {name: 'Tim', category: 'dev'},
|
|
||||||
* {name: 'Vincent', category: 'dev'},
|
|
||||||
* {name: 'AlexS', category: 'dev'}
|
|
||||||
* ],
|
|
||||||
* 'sales': [
|
|
||||||
* {name: 'Ben', category: 'sales'},
|
|
||||||
* {name: 'Jeremy', category: 'sales'},
|
|
||||||
* {name: 'AlexK', category: 'sales'}
|
|
||||||
* ]
|
|
||||||
* }
|
|
||||||
* @param {array} collection Array of objects to group
|
|
||||||
* @param {String} property The attribute on which apply the grouping
|
|
||||||
* @return {array}
|
|
||||||
* @throws Error when one of the element does not have the specified property
|
|
||||||
*/
|
|
||||||
groupBy(collection, property) {
|
|
||||||
const newCollection = {};
|
|
||||||
$.each(collection, (index, item) => {
|
|
||||||
if (item[property] === undefined) {
|
|
||||||
throw new Error(`[groupBy]: Object has no key ${property}`);
|
|
||||||
}
|
|
||||||
let key = item[property];
|
|
||||||
if (typeof key === 'string') {
|
|
||||||
key = key.toLowerCase();
|
|
||||||
}
|
|
||||||
// fix #171 the given data type of docsearch hits might be conflict with the properties of the native Object,
|
|
||||||
// such as the constructor, so we need to do this check.
|
|
||||||
if (!Object.prototype.hasOwnProperty.call(newCollection, key)) {
|
|
||||||
newCollection[key] = [];
|
|
||||||
}
|
|
||||||
newCollection[key].push(item);
|
|
||||||
});
|
|
||||||
return newCollection;
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
* Return an array of all the values of the specified object
|
|
||||||
* eg.
|
|
||||||
* values({
|
|
||||||
* foo: 42,
|
|
||||||
* bar: true,
|
|
||||||
* baz: 'yep'
|
|
||||||
* })
|
|
||||||
* =>
|
|
||||||
* [42, true, yep]
|
|
||||||
* @param {object} object Object to extract values from
|
|
||||||
* @return {array}
|
|
||||||
*/
|
|
||||||
values(object) {
|
|
||||||
return Object.keys(object).map((key) => object[key]);
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
* Flattens an array
|
|
||||||
* eg.
|
|
||||||
* flatten([1, 2, [3, 4], [5, 6]])
|
|
||||||
* =>
|
|
||||||
* [1, 2, 3, 4, 5, 6]
|
|
||||||
* @param {array} array Array to flatten
|
|
||||||
* @return {array}
|
|
||||||
*/
|
|
||||||
flatten(array) {
|
|
||||||
const results = [];
|
|
||||||
array.forEach((value) => {
|
|
||||||
if (!Array.isArray(value)) {
|
|
||||||
results.push(value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
value.forEach((subvalue) => {
|
|
||||||
results.push(subvalue);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
* Flatten all values of an object into an array, marking each first element of
|
|
||||||
* each group with a specific flag
|
|
||||||
* eg.
|
|
||||||
* flattenAndFlagFirst({
|
|
||||||
* 'devs': [
|
|
||||||
* {name: 'Tim', category: 'dev'},
|
|
||||||
* {name: 'Vincent', category: 'dev'},
|
|
||||||
* {name: 'AlexS', category: 'dev'}
|
|
||||||
* ],
|
|
||||||
* 'sales': [
|
|
||||||
* {name: 'Ben', category: 'sales'},
|
|
||||||
* {name: 'Jeremy', category: 'sales'},
|
|
||||||
* {name: 'AlexK', category: 'sales'}
|
|
||||||
* ]
|
|
||||||
* , 'isTop');
|
|
||||||
* =>
|
|
||||||
* [
|
|
||||||
* {name: 'Tim', category: 'dev', isTop: true},
|
|
||||||
* {name: 'Vincent', category: 'dev', isTop: false},
|
|
||||||
* {name: 'AlexS', category: 'dev', isTop: false},
|
|
||||||
* {name: 'Ben', category: 'sales', isTop: true},
|
|
||||||
* {name: 'Jeremy', category: 'sales', isTop: false},
|
|
||||||
* {name: 'AlexK', category: 'sales', isTop: false}
|
|
||||||
* ]
|
|
||||||
* @param {object} object Object to flatten
|
|
||||||
* @param {string} flag Flag to set to true on first element of each group
|
|
||||||
* @return {array}
|
|
||||||
*/
|
|
||||||
flattenAndFlagFirst(object, flag) {
|
|
||||||
const values = this.values(object).map((collection) =>
|
|
||||||
collection.map((item, index) => {
|
|
||||||
// eslint-disable-next-line no-param-reassign
|
|
||||||
item[flag] = index === 0;
|
|
||||||
return item;
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
return this.flatten(values);
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
* Removes all empty strings, null, false and undefined elements array
|
|
||||||
* eg.
|
|
||||||
* compact([42, false, null, undefined, '', [], 'foo']);
|
|
||||||
* =>
|
|
||||||
* [42, [], 'foo']
|
|
||||||
* @param {array} array Array to compact
|
|
||||||
* @return {array}
|
|
||||||
*/
|
|
||||||
compact(array) {
|
|
||||||
const results = [];
|
|
||||||
array.forEach((value) => {
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
results.push(value);
|
|
||||||
});
|
|
||||||
return results;
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
* Returns the highlighted value of the specified key in the specified object.
|
|
||||||
* If no highlighted value is available, will return the key value directly
|
|
||||||
* eg.
|
|
||||||
* getHighlightedValue({
|
|
||||||
* _highlightResult: {
|
|
||||||
* text: {
|
|
||||||
* value: '<mark>foo</mark>'
|
|
||||||
* }
|
|
||||||
* },
|
|
||||||
* text: 'foo'
|
|
||||||
* }, 'text');
|
|
||||||
* =>
|
|
||||||
* '<mark>foo</mark>'
|
|
||||||
* @param {object} object Hit object returned by the Algolia API
|
|
||||||
* @param {string} property Object key to look for
|
|
||||||
* @return {string}
|
|
||||||
**/
|
|
||||||
getHighlightedValue(object, property) {
|
|
||||||
if (
|
|
||||||
object._highlightResult &&
|
|
||||||
object._highlightResult.hierarchy_camel &&
|
|
||||||
object._highlightResult.hierarchy_camel[property] &&
|
|
||||||
object._highlightResult.hierarchy_camel[property].matchLevel &&
|
|
||||||
object._highlightResult.hierarchy_camel[property].matchLevel !== 'none' &&
|
|
||||||
object._highlightResult.hierarchy_camel[property].value
|
|
||||||
) {
|
|
||||||
return object._highlightResult.hierarchy_camel[property].value;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
object._highlightResult &&
|
|
||||||
object._highlightResult &&
|
|
||||||
object._highlightResult[property] &&
|
|
||||||
object._highlightResult[property].value
|
|
||||||
) {
|
|
||||||
return object._highlightResult[property].value;
|
|
||||||
}
|
|
||||||
return object[property];
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
* Returns the snippeted value of the specified key in the specified object.
|
|
||||||
* If no highlighted value is available, will return the key value directly.
|
|
||||||
* Will add starting and ending ellipsis (…) if we detect that a sentence is
|
|
||||||
* incomplete
|
|
||||||
* eg.
|
|
||||||
* getSnippetedValue({
|
|
||||||
* _snippetResult: {
|
|
||||||
* text: {
|
|
||||||
* value: '<mark>This is an unfinished sentence</mark>'
|
|
||||||
* }
|
|
||||||
* },
|
|
||||||
* text: 'This is an unfinished sentence'
|
|
||||||
* }, 'text');
|
|
||||||
* =>
|
|
||||||
* '<mark>This is an unfinished sentence</mark>…'
|
|
||||||
* @param {object} object Hit object returned by the Algolia API
|
|
||||||
* @param {string} property Object key to look for
|
|
||||||
* @return {string}
|
|
||||||
**/
|
|
||||||
getSnippetedValue(object, property) {
|
|
||||||
if (!object._snippetResult || !object._snippetResult[property] || !object._snippetResult[property].value) {
|
|
||||||
return object[property];
|
|
||||||
}
|
|
||||||
let snippet = object._snippetResult[property].value;
|
|
||||||
|
|
||||||
if (snippet[0] !== snippet[0].toUpperCase()) {
|
|
||||||
snippet = `…${snippet}`;
|
|
||||||
}
|
|
||||||
if (['.', '!', '?'].indexOf(snippet[snippet.length - 1]) === -1) {
|
|
||||||
snippet = `${snippet}…`;
|
|
||||||
}
|
|
||||||
return snippet;
|
|
||||||
},
|
|
||||||
/*
|
|
||||||
* Deep clone an object.
|
|
||||||
* Note: This will not clone functions and dates
|
|
||||||
* @param {object} object Object to clone
|
|
||||||
* @return {object}
|
|
||||||
*/
|
|
||||||
deepClone(object) {
|
|
||||||
return JSON.parse(JSON.stringify(object));
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default utils;
|
|
Loading…
x
Reference in New Issue
Block a user