5
0
mirror of https://github.com/wailsapp/wails.git synced 2025-05-02 10:39:50 +08:00

Use local search (#1234)

This commit is contained in:
Lea Anthony 2022-03-09 22:30:24 +11:00 committed by GitHub
parent 9097c9086b
commit ba0b173e02
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 10378 additions and 17027 deletions

View File

@ -29,12 +29,14 @@ const darkCodeTheme = require("prism-react-renderer/themes/palenight");
},
},
plugins: [
[
"docusaurus-plugin-plausible",
["docusaurus-plugin-plausible",
{
domain: "wails.io",
},
],
[require.resolve('docusaurus-lunr-search'), {
languages: ['en', 'zh'] // language codes
}]
],
presets: [
[
@ -170,29 +172,7 @@ const darkCodeTheme = require("prism-react-renderer/themes/palenight");
theme: lightCodeTheme,
darkTheme: darkCodeTheme,
},
algolia: {
// The application ID provided by Algolia
appId: "AWTCNFZ4FF",
// Public API key: it is safe to commit it
apiKey: "d6495e0dda237daa037967b3809e4089",
indexName: "wails",
// Optional: see doc section below
contextualSearch: true,
// Optional: Specify domains where the navigation should occur through window.location instead on history.push. Useful when our Algolia config crawls multiple documentation sites and we want to navigate with window.location.href to them.
// externalUrlRegex: 'external\\.com|domain\\.com',
// Optional: Algolia search parameters
searchParameters: {},
// Optional: path for search page that enabled by default (`false` to disable it)
searchPagePath: "false",
//... other Algolia params
},
}),
}
);

17441
website/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -22,8 +22,10 @@
"@mdx-js/react": "^1.6.21",
"@svgr/webpack": "^5.5.0",
"clsx": "^1.1.1",
"docusaurus-lunr-search": "^2.1.15",
"docusaurus-plugin-plausible": "0.0.5",
"file-loader": "^6.2.0",
"nodejieba": "^2.6.0",
"prism-react-renderer": "^1.2.1",
"react": "^17.0.1",
"react-contributors": "^1.1.2",

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,119 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import React, {useCallback, useRef, useState} from "react";
import classnames from "classnames";
import {useHistory} from "@docusaurus/router";
import useDocusaurusContext from "@docusaurus/useDocusaurusContext";
import {usePluginData} from '@docusaurus/useGlobalData';
const Search = props => {
const initialized = useRef(false);
const searchBarRef = useRef(null);
const [indexReady, setIndexReady] = useState(false);
const history = useHistory();
const {siteConfig = {}, isClient = false} = useDocusaurusContext();
const {baseUrl} = siteConfig;
const initAlgolia = (searchDocs, searchIndex, DocSearch) => {
new DocSearch({
searchDocs,
searchIndex,
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 = baseUrl + 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("./lib/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 (isClient) {
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;

View File

@ -0,0 +1,307 @@
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";
/**
* Adds an autocomplete dropdown to an input field
* @function DocSearch
* @param {Object} options.searchDocs Search Documents
* @param {Object} options.searchIndex Lune searchIndexes
* @param {string} options.inputSelector CSS selector that targets the input
* value.
* @param {Object} [options.autocompleteOptions] Options to pass to the underlying autocomplete instance
* @return {Object}
*/
class DocSearch {
constructor({
searchDocs,
searchIndex,
inputSelector,
debug = false,
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);
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;
}
// 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
};
});
}
// Given a list of hits returned by the API, will reformat them to be used in
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);
}
/**
* 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));
});
};
}
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;

View File

@ -0,0 +1,149 @@
import lunr from "@generated/lunr.client";
lunr.tokenizer.separator = /[\s\-/]+/;
class LunrSearchAdapter {
constructor(searchDocs, searchIndex) {
this.searchDocs = searchDocs;
this.lunrIndex = lunr.Index.load(searchIndex);
}
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;

View File

@ -0,0 +1,114 @@
const prefix = 'algolia-docsearch';
const suggestionPrefix = `${prefix}-suggestion`;
const footerPrefix = `${prefix}-footer`;
/* eslint-disable max-len */
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;

View File

@ -0,0 +1,270 @@
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;

View File

@ -0,0 +1,40 @@
/**
* Copyright (c) 2017-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.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;
}
}

8395
website/yarn.lock Normal file

File diff suppressed because it is too large Load Diff