1
0
Fork 0
docsearch/src/DocSearch.tsx
François Chalifour fa42ab2bd9 feat(docsearch): add DocSearch for Docusaurus (#39)
Co-authored-by: eunjae-lee <karis612@gmail.com>
Co-authored-by: Shipow <shipowlata@gmail.com>
2020-03-31 17:08:28 +02:00

235 lines
6.9 KiB
TypeScript

import React, { useRef, useEffect } from 'react';
import {
createAutocomplete,
AutocompleteState,
} from '@francoischalifour/autocomplete-core';
import { getAlgoliaHits } from '@francoischalifour/autocomplete-preset-algolia';
import { DocSearchHit, InternalDocSearchHit } from './types';
import { createSearchClient, groupBy, noop } from './utils';
import { SearchBox } from './SearchBox';
import { Dropdown } from './Dropdown';
import { Footer } from './Footer';
interface DocSearchProps {
appId?: string;
apiKey: string;
indexName: string;
searchParameters: any;
onClose(): void;
}
export function DocSearch({
appId = 'BH4D9OD16A',
apiKey,
indexName,
searchParameters,
onClose = noop,
}: DocSearchProps) {
const [state, setState] = React.useState<
AutocompleteState<InternalDocSearchHit>
>({
query: '',
suggestions: [],
} as any);
const searchBoxRef = useRef<HTMLDivElement | null>(null);
const dropdownRef = useRef<HTMLDivElement | null>(null);
const inputRef = useRef<HTMLInputElement | null>(null);
const snipetLength = useRef<number>(10);
const searchClient = React.useMemo(() => createSearchClient(appId, apiKey), [
appId,
apiKey,
]);
const {
getEnvironmentProps,
getRootProps,
getFormProps,
getLabelProps,
getInputProps,
getMenuProps,
getItemProps,
} = React.useMemo(
() =>
createAutocomplete<
InternalDocSearchHit,
React.FormEvent<HTMLFormElement>,
React.MouseEvent,
React.KeyboardEvent
>({
defaultHighlightedIndex: 0,
autoFocus: true,
placeholder: 'Search docs...',
openOnFocus: true,
onStateChange({ state }) {
setState(state as any);
},
getSources({ query }) {
return getAlgoliaHits({
searchClient,
queries: [
{
indexName,
query,
params: {
attributesToRetrieve: [
'hierarchy.lvl0',
'hierarchy.lvl1',
'hierarchy.lvl2',
'hierarchy.lvl3',
'hierarchy.lvl4',
'hierarchy.lvl5',
'hierarchy.lvl6',
'content',
'type',
'url',
],
attributesToSnippet: [
`hierarchy.lvl1:${snipetLength.current}`,
`hierarchy.lvl2:${snipetLength.current}`,
`hierarchy.lvl3:${snipetLength.current}`,
`hierarchy.lvl4:${snipetLength.current}`,
`hierarchy.lvl5:${snipetLength.current}`,
`hierarchy.lvl6:${snipetLength.current}`,
`content:${snipetLength.current}`,
],
snippetEllipsisText: '…',
highlightPreTag: '<mark>',
highlightPostTag: '</mark>',
hitsPerPage: 20,
distinct: 4,
...searchParameters,
},
},
],
}).then((hits: DocSearchHit[]) => {
const formattedHits = hits.map(hit => {
const url = new URL(hit.url);
return {
...hit,
url: hit.url
// @TODO: temporary convenience for development.
.replace(url.origin, '')
.replace('#__docusaurus', ''),
};
});
const sources = groupBy(formattedHits, hit => hit.hierarchy.lvl0);
return Object.values<DocSearchHit[]>(sources).map(items => {
return {
onSelect() {
onClose();
},
getSuggestionUrl({ suggestion }) {
return suggestion.url;
},
getSuggestions() {
return Object.values(
groupBy(items, item => item.hierarchy.lvl1)
)
.map(hits =>
hits.map(item => {
return {
...item,
// eslint-disable-next-line @typescript-eslint/camelcase
__docsearch_parent:
item.type !== 'lvl1' &&
hits.find(
siblingItem =>
siblingItem.type === 'lvl1' &&
siblingItem.hierarchy.lvl1 ===
item.hierarchy.lvl1
),
};
})
)
.flat();
},
};
});
});
},
}),
[indexName, searchParameters, searchClient, onClose]
);
useEffect(() => {
const isMobileMediaQuery = window.matchMedia('(max-width: 750px)');
if (isMobileMediaQuery.matches) {
snipetLength.current = 5;
}
}, []);
useEffect(() => {
if (dropdownRef.current) {
dropdownRef.current.scrollTop = 0;
}
}, [state.query]);
useEffect(() => {
if (!(searchBoxRef.current && dropdownRef.current && inputRef.current)) {
return undefined;
}
const { onTouchStart, onTouchMove } = getEnvironmentProps({
searchBoxElement: searchBoxRef.current,
dropdownElement: dropdownRef.current,
inputElement: inputRef.current,
});
window.addEventListener('touchstart', onTouchStart);
window.addEventListener('touchmove', onTouchMove);
return () => {
window.removeEventListener('touchstart', onTouchStart);
window.removeEventListener('touchmove', onTouchMove);
};
}, [getEnvironmentProps, searchBoxRef, dropdownRef, inputRef]);
return (
<div
className={[
'DocSearch-Container',
state.status === 'stalled' && 'DocSearch-Container--Stalled',
state.status === 'error' && 'DocSearch-Container--Errored',
]
.filter(Boolean)
.join(' ')}
{...getRootProps({
onClick(event: React.MouseEvent) {
if (event.target === event.currentTarget) {
onClose();
}
},
})}
>
<div className="DocSearch-Modal">
<header className="DocSearch-SearchBar" ref={searchBoxRef}>
<SearchBox
inputRef={inputRef}
query={state.query}
getFormProps={getFormProps}
getLabelProps={getLabelProps}
getInputProps={getInputProps}
onClose={onClose}
/>
</header>
<div className="DocSearch-Dropdown" ref={dropdownRef}>
<Dropdown
state={state}
getMenuProps={getMenuProps}
getItemProps={getItemProps}
/>
</div>
<footer className="DocSearch-Footer">
<Footer />
</footer>
</div>
</div>
);
}