feat(v2): add search page (#2756)

This commit is contained in:
Alexey Pyltsyn 2020-05-17 10:55:40 +03:00 committed by GitHub
parent 1fe2dc192e
commit 3ad4550854
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 688 additions and 38 deletions

View file

@ -46,6 +46,7 @@ import {
} from './types'; } from './types';
import {Configuration} from 'webpack'; import {Configuration} from 'webpack';
import {docsVersion} from './version'; import {docsVersion} from './version';
import {VERSIONS_JSON_FILE} from './constants';
const DEFAULT_OPTIONS: PluginOptions = { const DEFAULT_OPTIONS: PluginOptions = {
path: 'docs', // Path to data on filesystem, relative to site dir. path: 'docs', // Path to data on filesystem, relative to site dir.
@ -95,6 +96,10 @@ export default function pluginContentDocs(
return { return {
name: 'docusaurus-plugin-content-docs', name: 'docusaurus-plugin-content-docs',
getThemePath() {
return path.resolve(__dirname, './theme');
},
extendCli(cli) { extendCli(cli) {
cli cli
.command('docs:version') .command('docs:version')
@ -418,7 +423,16 @@ export default function pluginContentDocs(
configureWebpack(_config, isServer, utils) { configureWebpack(_config, isServer, utils) {
const {getBabelLoader, getCacheLoader} = utils; const {getBabelLoader, getCacheLoader} = utils;
const {rehypePlugins, remarkPlugins} = options; const {rehypePlugins, remarkPlugins} = options;
// Suppress warnings about non-existing of versions file.
const stats = {
warningsFilter: [VERSIONS_JSON_FILE],
};
return { return {
stats,
devServer: {
stats,
},
resolve: { resolve: {
alias: { alias: {
'~docs': dataDir, '~docs': dataDir,

View file

@ -0,0 +1,22 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
let versions: string[] = [];
try {
versions = require('@site/versions.json');
} catch (e) {}
function useVersioning() {
return {
versioningEnabled: versions.length > 0,
versions,
latestVersion: versions[0],
};
}
export default useVersioning;

View file

@ -8,11 +8,15 @@
}, },
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"algoliasearch": "^3.24.5",
"algoliasearch-helper": "^3.1.1",
"classnames": "^2.2.6", "classnames": "^2.2.6",
"docsearch.js": "^2.6.3" "docsearch.js": "^2.6.3",
"eta": "^1.1.1"
}, },
"peerDependencies": { "peerDependencies": {
"@docusaurus/core": "^2.0.0", "@docusaurus/core": "^2.0.0",
"@docusaurus/utils": "^2.0.0-alpha.54",
"react": "^16.8.4", "react": "^16.8.4",
"react-dom": "^16.8.4" "react-dom": "^16.8.4"
}, },

View file

@ -6,8 +6,20 @@
*/ */
const path = require('path'); const path = require('path');
const fs = require('fs');
const eta = require('eta');
const {normalizeUrl} = require('@docusaurus/utils');
const openSearchTemplate = require('./templates/opensearch');
const OPEN_SEARCH_FILENAME = 'opensearch.xml';
module.exports = function (context) {
const {
baseUrl,
siteConfig: {title, url, favicon},
} = context;
const pagePath = path.resolve(__dirname, './pages/search/index.js');
module.exports = function () {
return { return {
name: 'docusaurus-theme-search-algolia', name: 'docusaurus-theme-search-algolia',
@ -15,6 +27,10 @@ module.exports = function () {
return path.resolve(__dirname, './theme'); return path.resolve(__dirname, './theme');
}, },
getPathsToWatch() {
return [pagePath];
},
configureWebpack() { configureWebpack() {
// Ensure that algolia docsearch styles is its own chunk. // Ensure that algolia docsearch styles is its own chunk.
return { return {
@ -34,5 +50,44 @@ module.exports = function () {
}, },
}; };
}, },
async contentLoaded({actions: {addRoute}}) {
addRoute({
path: normalizeUrl([baseUrl, 'search']),
component: pagePath,
exact: true,
});
},
async postBuild({outDir}) {
try {
fs.writeFileSync(
path.join(outDir, OPEN_SEARCH_FILENAME),
eta.render(openSearchTemplate.trim(), {
title,
url,
favicon: normalizeUrl([url, favicon]),
}),
);
} catch (err) {
throw new Error(`Generating OpenSearch file failed: ${err}`);
}
},
injectHtmlTags() {
return {
headTags: [
{
tagName: 'link',
attributes: {
rel: 'search',
type: 'application/opensearchdescription+xml',
title,
href: normalizeUrl([baseUrl, OPEN_SEARCH_FILENAME]),
},
},
],
};
},
}; };
}; };

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,104 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
.searchQueryInput,
.searchVersionInput {
border-radius: var(--ifm-global-radius);
border: var(--ifm-global-border-width) solid
var(--ifm-color-content-secondary);
font-size: var(--ifm-font-size-base);
padding: 0.5rem;
width: 100%;
background: #fff;
}
.searchResultsColumn {
font-size: 0.9rem;
}
.searchLogoColumn {
text-align: right;
}
.algoliaLogo {
max-width: 150px;
}
.algoliaLogoPathFill {
fill: var(--ifm-font-color-base);
}
.searchResultItem {
padding: 1rem 0;
border-bottom: 1px solid #dfe3e8;
}
.searchResultItemHeading {
font-size: var(--ifm-h2-font-size);
}
.searchResultItemPath {
font-size: 0.8rem;
color: var(--ifm-color-content-secondary);
display: block;
}
.searchResultItemSummary {
margin: 0.5rem 0 0 0;
font-style: italic;
}
@media only screen and (max-width: 996px) {
.searchQueryColumn {
max-width: 60% !important;
}
.searchVersionColumn {
max-width: 40% !important;
}
.algoliaLogo {
width: 100%;
}
.searchResultsColumn {
max-width: 60% !important;
}
.searchLogoColumn {
overflow: hidden;
max-width: 40% !important;
padding-left: 0 !important;
}
}
.loadingSpinner {
pointer-events: none;
width: 3rem;
height: 3rem;
border: 0.4em solid transparent;
border-color: #eee;
border-top-color: var(--ifm-color-primary);
border-radius: 50%;
animation: loadingspin 1s linear infinite;
margin: 0 auto;
}
@keyframes loadingspin {
100% {
transform: rotate(360deg);
}
}
.loader {
margin-top: 2rem;
}
:global(.search-result-match) {
background: rgba(255, 215, 142, 0.22);
padding: 0.09em 0;
}

View file

@ -0,0 +1,20 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
module.exports = `
<?xml version="1.0" encoding="UTF-8"?>
<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
xmlns:moz="http://www.mozilla.org/2006/browser/search/">
<ShortName><%= it.title %></ShortName>
<Description>Search <%= it.title %></Description>
<InputEncoding>UTF-8</InputEncoding>
<Image width="16" height="16" type="image/x-icon"><%= it.favicon %></Image>
<Url type="text/html" method="get" template="<%= it.url %>/search?q={searchTerms}"/>
<Url type="application/opensearchdescription+xml" rel="self" template="<%= it.url %>/opensearch.xml" />
<moz:SearchForm><%= it.url %></moz:SearchForm>
</OpenSearchDescription>
`;

View file

@ -10,6 +10,7 @@ import classnames from 'classnames';
import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import {useHistory} from '@docusaurus/router'; import {useHistory} from '@docusaurus/router';
import useSearchQuery from '@theme/hooks/useSearchQuery';
import './styles.css'; import './styles.css';
@ -21,6 +22,7 @@ const Search = (props) => {
themeConfig: {algolia}, themeConfig: {algolia},
} = siteConfig; } = siteConfig;
const history = useHistory(); const history = useHistory();
const {navigateToSearchPage} = useSearchQuery();
function initAlgolia(focus) { function initAlgolia(focus) {
window.docsearch({ window.docsearch({
@ -29,6 +31,9 @@ const Search = (props) => {
indexName: algolia.indexName, indexName: algolia.indexName,
inputSelector: '#search_input_react', inputSelector: '#search_input_react',
algoliaOptions: algolia.algoliaOptions, algoliaOptions: algolia.algoliaOptions,
autocompleteOptions: {
autoselect: false,
},
// Override algolia's default selection event, allowing us to do client-side // Override algolia's default selection event, allowing us to do client-side
// navigation and avoiding a full page refresh. // navigation and avoiding a full page refresh.
handleSelected: (_input, _event, suggestion) => { handleSelected: (_input, _event, suggestion) => {
@ -87,6 +92,12 @@ const Search = (props) => {
loadAlgolia(needFocus); loadAlgolia(needFocus);
}); });
const handleSearchInputPressEnter = useCallback((e) => {
if (e.key === 'Enter') {
navigateToSearchPage(e.target.value);
}
});
return ( return (
<div className="navbar__search" key="search-box"> <div className="navbar__search" key="search-box">
<span <span
@ -112,6 +123,7 @@ const Search = (props) => {
onMouseOver={handleSearchInput} onMouseOver={handleSearchInput}
onFocus={handleSearchInput} onFocus={handleSearchInput}
onBlur={handleSearchInputBlur} onBlur={handleSearchInputBlur}
onKeyDown={handleSearchInputPressEnter}
ref={searchBarRef} ref={searchBarRef}
/> />
</div> </div>

View file

@ -0,0 +1,41 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import {useHistory, useLocation} from '@docusaurus/router';
import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment';
const SEARCH_PARAM_QUERY = 'q';
function useSearchQuery() {
const history = useHistory();
const location = useLocation();
return {
searchValue:
(ExecutionEnvironment.canUseDOM &&
new URLSearchParams(location.search).get(SEARCH_PARAM_QUERY)) ||
'',
updateSearchPath: (searchValue) => {
const searchParams = new URLSearchParams(location.search);
if (searchValue) {
searchParams.set(SEARCH_PARAM_QUERY, searchValue);
} else {
searchParams.delete(SEARCH_PARAM_QUERY);
}
history.replace({
search: searchParams.toString(),
});
},
navigateToSearchPage: (searchValue) => {
history.push(`/search?q=${searchValue}`);
},
};
}
export default useSearchQuery;

View file

@ -10,7 +10,7 @@ import CopyWebpackPlugin from 'copy-webpack-plugin';
import fs from 'fs-extra'; import fs from 'fs-extra';
import path from 'path'; import path from 'path';
import ReactLoadableSSRAddon from 'react-loadable-ssr-addon'; import ReactLoadableSSRAddon from 'react-loadable-ssr-addon';
import webpack, {Configuration, Plugin} from 'webpack'; import webpack, {Configuration, Plugin, Stats} from 'webpack';
import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer';
import merge from 'webpack-merge'; import merge from 'webpack-merge';
import {STATIC_DIR_NAME} from '../constants'; import {STATIC_DIR_NAME} from '../constants';
@ -35,7 +35,18 @@ function compile(config: Configuration[]): Promise<any> {
reject(new Error('Failed to compile with errors.')); reject(new Error('Failed to compile with errors.'));
} }
if (stats.hasWarnings()) { if (stats.hasWarnings()) {
stats.toJson('errors-warnings').warnings.forEach((warning) => { // Custom filtering warnings (see https://github.com/webpack/webpack/issues/7841).
let warnings = stats.toJson('errors-warnings').warnings;
const warningsFilter = ((config[0].stats as Stats.ToJsonOptionsObject)
?.warningsFilter || []) as any[];
if (Array.isArray(warningsFilter)) {
warnings = warnings.filter((warning) =>
warningsFilter.every((str) => !warning.includes(str)),
);
}
warnings.forEach((warning) => {
console.warn(warning); console.warn(warning);
}); });
} }

View file

@ -124,42 +124,48 @@ export async function start(
// https://webpack.js.org/configuration/dev-server // https://webpack.js.org/configuration/dev-server
const devServerConfig: WebpackDevServer.Configuration = { const devServerConfig: WebpackDevServer.Configuration = {
compress: true, ...{
clientLogLevel: 'error', compress: true,
hot: true, clientLogLevel: 'error',
hotOnly: cliOptions.hotOnly, hot: true,
// Use 'ws' instead of 'sockjs-node' on server since we're using native hotOnly: cliOptions.hotOnly,
// websockets in `webpackHotDevClient`. // Use 'ws' instead of 'sockjs-node' on server since we're using native
transportMode: 'ws', // websockets in `webpackHotDevClient`.
// Prevent a WS client from getting injected as we're already including transportMode: 'ws',
// `webpackHotDevClient`. // Prevent a WS client from getting injected as we're already including
injectClient: false, // `webpackHotDevClient`.
quiet: true, injectClient: false,
headers: { quiet: true,
'access-control-allow-origin': '*', headers: {
}, 'access-control-allow-origin': '*',
publicPath: baseUrl, },
watchOptions: { publicPath: baseUrl,
ignored: /node_modules/, watchOptions: {
poll: cliOptions.poll, ignored: /node_modules/,
}, poll: cliOptions.poll,
historyApiFallback: { },
rewrites: [{from: /\/*/, to: baseUrl}], historyApiFallback: {
}, rewrites: [{from: /\/*/, to: baseUrl}],
disableHostCheck: true, },
// Disable overlay on browser since we use CRA's overlay error reporting. disableHostCheck: true,
overlay: false, // Disable overlay on browser since we use CRA's overlay error reporting.
host, overlay: false,
before: (app, server) => { host,
app.use(baseUrl, express.static(path.resolve(siteDir, STATIC_DIR_NAME))); before: (app, server) => {
app.use(
baseUrl,
express.static(path.resolve(siteDir, STATIC_DIR_NAME)),
);
// This lets us fetch source contents from webpack for the error overlay. // This lets us fetch source contents from webpack for the error overlay.
app.use(evalSourceMapMiddleware(server)); app.use(evalSourceMapMiddleware(server));
// This lets us open files from the runtime error overlay. // This lets us open files from the runtime error overlay.
app.use(errorOverlayMiddleware()); app.use(errorOverlayMiddleware());
// TODO: add plugins beforeDevServer and afterDevServer hook // TODO: add plugins beforeDevServer and afterDevServer hook
},
}, },
...config.devServer,
}; };
const compiler = webpack(config); const compiler = webpack(config);
const devServer = new WebpackDevServer(compiler, devServerConfig); const devServer = new WebpackDevServer(compiler, devServerConfig);

View file

@ -3182,6 +3182,13 @@ ajv@^6.10.2, ajv@^6.12.0, ajv@^6.5.5:
json-schema-traverse "^0.4.1" json-schema-traverse "^0.4.1"
uri-js "^4.2.2" uri-js "^4.2.2"
algoliasearch-helper@^3.1.1:
version "3.1.1"
resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-3.1.1.tgz#4cdfbaed6670d82626ac1fae001ccbf53192f497"
integrity sha512-Jkqlp8jezQRixf7sbQ2zFXHpdaT41g9sHBqT6pztv5nfDmg94K+pwesAy6UbxRY78IL54LIaV1FLttMtT+IzzA==
dependencies:
events "^1.1.1"
algoliasearch@^3.24.5: algoliasearch@^3.24.5:
version "3.35.1" version "3.35.1"
resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.35.1.tgz#297d15f534a3507cab2f5dfb996019cac7568f0c" resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.35.1.tgz#297d15f534a3507cab2f5dfb996019cac7568f0c"
@ -6728,7 +6735,7 @@ eventemitter3@^4.0.0:
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb"
integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg==
events@^1.1.0: events@^1.1.0, events@^1.1.1:
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924"
integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ= integrity sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=