feat(docsearch): add recent searches (#40)
This commit is contained in:
parent
ba6bdde041
commit
4c0aa3b71e
13 changed files with 430 additions and 123 deletions
|
|
@ -5,12 +5,18 @@ import {
|
|||
} from '@francoischalifour/autocomplete-core';
|
||||
import { getAlgoliaHits } from '@francoischalifour/autocomplete-preset-algolia';
|
||||
|
||||
import { DocSearchHit, InternalDocSearchHit } from './types';
|
||||
import {
|
||||
DocSearchHit,
|
||||
InternalDocSearchHit,
|
||||
RecentDocSearchHit,
|
||||
} from './types';
|
||||
import { createSearchClient, groupBy, noop } from './utils';
|
||||
import { SearchBox } from './SearchBox';
|
||||
import { Dropdown } from './Dropdown';
|
||||
import { Footer } from './Footer';
|
||||
|
||||
import { createRecentSearches } from './recent-searches';
|
||||
|
||||
interface DocSearchProps {
|
||||
appId?: string;
|
||||
apiKey: string;
|
||||
|
|
@ -42,18 +48,9 @@ export function DocSearch({
|
|||
appId,
|
||||
apiKey,
|
||||
]);
|
||||
const recentSearches = useRef(createRecentSearches<RecentDocSearchHit>());
|
||||
|
||||
const {
|
||||
getEnvironmentProps,
|
||||
getRootProps,
|
||||
getFormProps,
|
||||
getLabelProps,
|
||||
getInputProps,
|
||||
getMenuProps,
|
||||
getItemProps,
|
||||
setQuery,
|
||||
refresh,
|
||||
} = React.useMemo(
|
||||
const autocomplete = React.useMemo(
|
||||
() =>
|
||||
createAutocomplete<
|
||||
InternalDocSearchHit,
|
||||
|
|
@ -127,9 +124,27 @@ export function DocSearch({
|
|||
});
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
return [
|
||||
{
|
||||
onSelect({ suggestion }) {
|
||||
recentSearches.current.saveSearch(suggestion);
|
||||
onClose();
|
||||
},
|
||||
getSuggestionUrl({ suggestion }) {
|
||||
return suggestion.url;
|
||||
},
|
||||
getSuggestions() {
|
||||
return recentSearches.current.getSearches();
|
||||
},
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
return Object.values<DocSearchHit[]>(sources).map(items => {
|
||||
return {
|
||||
onSelect() {
|
||||
onSelect({ suggestion }) {
|
||||
recentSearches.current.saveSearch(suggestion);
|
||||
onClose();
|
||||
},
|
||||
getSuggestionUrl({ suggestion }) {
|
||||
|
|
@ -165,6 +180,8 @@ export function DocSearch({
|
|||
[indexName, searchParameters, searchClient, onClose]
|
||||
);
|
||||
|
||||
const { getEnvironmentProps, getRootProps } = autocomplete;
|
||||
|
||||
useEffect(() => {
|
||||
const isMobileMediaQuery = window.matchMedia('(max-width: 750px)');
|
||||
|
||||
|
|
@ -219,23 +236,26 @@ export function DocSearch({
|
|||
<div className="DocSearch-Modal">
|
||||
<header className="DocSearch-SearchBar" ref={searchBoxRef}>
|
||||
<SearchBox
|
||||
inputRef={inputRef}
|
||||
query={state.query}
|
||||
getFormProps={getFormProps}
|
||||
getLabelProps={getLabelProps}
|
||||
getInputProps={getInputProps}
|
||||
{...autocomplete}
|
||||
state={state}
|
||||
onClose={onClose}
|
||||
inputRef={inputRef}
|
||||
/>
|
||||
</header>
|
||||
|
||||
<div className="DocSearch-Dropdown" ref={dropdownRef}>
|
||||
<Dropdown
|
||||
inputRef={inputRef}
|
||||
{...autocomplete}
|
||||
state={state}
|
||||
getMenuProps={getMenuProps}
|
||||
getItemProps={getItemProps}
|
||||
setQuery={setQuery}
|
||||
refresh={refresh}
|
||||
inputRef={inputRef}
|
||||
onItemClick={item => {
|
||||
recentSearches.current.saveSearch(item);
|
||||
onClose();
|
||||
}}
|
||||
onAction={item => {
|
||||
recentSearches.current.deleteSearch(item);
|
||||
autocomplete.refresh();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,48 +1,49 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
AutocompleteApi,
|
||||
AutocompleteState,
|
||||
GetMenuProps,
|
||||
GetItemProps,
|
||||
} from '@francoischalifour/autocomplete-core';
|
||||
|
||||
import { InternalDocSearchHit } from '../types';
|
||||
import { Error } from '../Error';
|
||||
import { NoResults } from '../NoResults';
|
||||
import { InternalDocSearchHit, RecentDocSearchHit } from '../types';
|
||||
import { EmptyScreen } from '../EmptyScreen';
|
||||
import { Results } from '../Results';
|
||||
import { NoResults } from '../NoResults';
|
||||
import { Error } from '../Error';
|
||||
|
||||
interface DropdownProps {
|
||||
state: AutocompleteState<InternalDocSearchHit>;
|
||||
getMenuProps: GetMenuProps;
|
||||
getItemProps: GetItemProps<InternalDocSearchHit, React.MouseEvent>;
|
||||
setQuery(value: string): void;
|
||||
refresh(): Promise<void>;
|
||||
inputRef: React.MutableRefObject<HTMLInputElement>;
|
||||
interface DropdownProps<TItem>
|
||||
extends AutocompleteApi<
|
||||
TItem,
|
||||
React.FormEvent,
|
||||
React.MouseEvent,
|
||||
React.KeyboardEvent
|
||||
> {
|
||||
state: AutocompleteState<TItem>;
|
||||
onItemClick(search: RecentDocSearchHit): void;
|
||||
onAction(search: RecentDocSearchHit): void;
|
||||
inputRef: React.MutableRefObject<null | HTMLInputElement>;
|
||||
}
|
||||
|
||||
export function Dropdown(props: DropdownProps) {
|
||||
export function Dropdown(props: DropdownProps<InternalDocSearchHit>) {
|
||||
if (props.state.status === 'error') {
|
||||
return <Error />;
|
||||
}
|
||||
|
||||
if (
|
||||
props.state.status === 'idle' &&
|
||||
props.state.suggestions.every(source => source.items.length === 0)
|
||||
) {
|
||||
const hasSuggestions = props.state.suggestions.some(
|
||||
source => source.items.length > 0
|
||||
);
|
||||
|
||||
if (!props.state.query) {
|
||||
return (
|
||||
<NoResults
|
||||
setQuery={props.setQuery}
|
||||
refresh={props.refresh}
|
||||
state={props.state}
|
||||
inputRef={props.inputRef}
|
||||
<EmptyScreen
|
||||
{...(props as DropdownProps<any>)}
|
||||
hasSuggestions={hasSuggestions}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Results
|
||||
suggestions={props.state.suggestions}
|
||||
getMenuProps={props.getMenuProps}
|
||||
getItemProps={props.getItemProps}
|
||||
/>
|
||||
);
|
||||
if (props.state.status === 'idle' && hasSuggestions === false) {
|
||||
return <NoResults {...props} />;
|
||||
}
|
||||
|
||||
return <Results {...props} />;
|
||||
}
|
||||
|
|
|
|||
147
src/EmptyScreen/EmptyScreen.tsx
Normal file
147
src/EmptyScreen/EmptyScreen.tsx
Normal file
|
|
@ -0,0 +1,147 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
AutocompleteApi,
|
||||
AutocompleteState,
|
||||
} from '@francoischalifour/autocomplete-core';
|
||||
|
||||
import { RecentDocSearchHit } from '../types';
|
||||
|
||||
interface EmptyScreenProps
|
||||
extends AutocompleteApi<
|
||||
RecentDocSearchHit,
|
||||
React.FormEvent,
|
||||
React.MouseEvent,
|
||||
React.KeyboardEvent
|
||||
> {
|
||||
state: AutocompleteState<RecentDocSearchHit>;
|
||||
hasSuggestions: boolean;
|
||||
onItemClick(item: RecentDocSearchHit): void;
|
||||
onAction(search: RecentDocSearchHit): void;
|
||||
}
|
||||
|
||||
export function EmptyScreen(props: EmptyScreenProps) {
|
||||
if (props.state.status === 'idle' && props.hasSuggestions === false) {
|
||||
return (
|
||||
<div>
|
||||
<p>Select results and your history will appear here.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="DocSearch-Dropdown-Container">
|
||||
{props.state.suggestions.map(({ source, items }, index) => {
|
||||
return (
|
||||
<section key={['recent', index].join(':')} className="DocSearch-Hits">
|
||||
<div className="DocSearch-Hit-source">Recent</div>
|
||||
|
||||
<ul {...props.getMenuProps()}>
|
||||
{items.map(item => {
|
||||
return (
|
||||
<li
|
||||
key={['recent', item.objectID].join(':')}
|
||||
className="DocSearch-Hit"
|
||||
{...props.getItemProps({
|
||||
item,
|
||||
source,
|
||||
onClick() {
|
||||
props.onItemClick(item);
|
||||
},
|
||||
})}
|
||||
>
|
||||
<a href={item.url}>
|
||||
<div className="DocSearch-Hit-Container">
|
||||
<div className="DocSearch-Hit-icon">
|
||||
<svg width="20" height="20">
|
||||
<g
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<path d="M3.18 6.6a8.23 8.23 0 1112.93 9.94h0a8.23 8.23 0 01-11.63 0" />
|
||||
<path d="M6.44 7.25H2.55V3.36M10.45 6v5.6M10.45 11.6L13 13" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
{item.hierarchy[item.type] && item.type === 'lvl1' && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<span className="DocSearch-Hit-title">
|
||||
{item.hierarchy.lvl1}
|
||||
</span>
|
||||
{item.content && (
|
||||
<span className="DocSearch-Hit-path">
|
||||
{item.content}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.hierarchy[item.type] &&
|
||||
(item.type === 'lvl2' ||
|
||||
item.type === 'lvl3' ||
|
||||
item.type === 'lvl4' ||
|
||||
item.type === 'lvl5' ||
|
||||
item.type === 'lvl6') && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<span className="DocSearch-Hit-title">
|
||||
{item.hierarchy[item.type]}
|
||||
</span>
|
||||
<span className="DocSearch-Hit-path">
|
||||
{item.hierarchy.lvl1}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.type === 'content' && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<span className="DocSearch-Hit-title">
|
||||
{item.content}
|
||||
</span>
|
||||
<span className="DocSearch-Hit-path">
|
||||
{item.hierarchy.lvl1}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="DocSearch-Hit-action">
|
||||
<button
|
||||
className="DocSearch-Hit-action-button"
|
||||
title="Delete this search"
|
||||
onClick={event => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
props.onAction(item);
|
||||
}}
|
||||
>
|
||||
<svg width="20" height="20">
|
||||
<g
|
||||
stroke="currentColor"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path
|
||||
d="M10,10 L15.0853291,4.91467086 L10,10 L15.0853291,15.0853291 L10,10 Z M10,10 L4.91467086,4.91467086 L10,10 L4.91467086,15.0853291 L10,10 Z"
|
||||
transform="translate(10.000000, 10.000000) rotate(-360.000000) translate(-10.000000, -10.000000) "
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</section>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
1
src/EmptyScreen/index.ts
Normal file
1
src/EmptyScreen/index.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
export * from './EmptyScreen';
|
||||
|
|
@ -1,11 +1,20 @@
|
|||
import React from 'react';
|
||||
import { AutocompleteState } from '@francoischalifour/autocomplete-core';
|
||||
import {
|
||||
AutocompleteApi,
|
||||
AutocompleteState,
|
||||
} from '@francoischalifour/autocomplete-core';
|
||||
|
||||
interface NoResultsProps {
|
||||
state: AutocompleteState<any>;
|
||||
setQuery(value: string): void;
|
||||
refresh(): Promise<void>;
|
||||
inputRef: React.MutableRefObject<HTMLInputElement>;
|
||||
import { InternalDocSearchHit } from '../types';
|
||||
|
||||
interface NoResultsProps
|
||||
extends AutocompleteApi<
|
||||
InternalDocSearchHit,
|
||||
React.FormEvent,
|
||||
React.MouseEvent,
|
||||
React.KeyboardEvent
|
||||
> {
|
||||
state: AutocompleteState<InternalDocSearchHit>;
|
||||
inputRef: React.MutableRefObject<null | HTMLInputElement>;
|
||||
}
|
||||
|
||||
export function NoResults(props: NoResultsProps) {
|
||||
|
|
@ -30,7 +39,7 @@ export function NoResults(props: NoResultsProps) {
|
|||
onClick={() => {
|
||||
props.setQuery(search.toLowerCase() + ' ');
|
||||
props.refresh();
|
||||
props.inputRef.current.focus();
|
||||
props.inputRef.current!.focus();
|
||||
}}
|
||||
>
|
||||
{search}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,34 @@
|
|||
import React from 'react';
|
||||
import {
|
||||
GetMenuProps,
|
||||
GetItemProps,
|
||||
AutocompleteSuggestion,
|
||||
AutocompleteApi,
|
||||
AutocompleteState,
|
||||
} from '@francoischalifour/autocomplete-core';
|
||||
|
||||
import { Snippet } from '../Snippet';
|
||||
import { SourceIcon } from './SourceIcon';
|
||||
import { SelectIcon } from './ActionIcon';
|
||||
import { InternalDocSearchHit } from '../types';
|
||||
|
||||
interface ResultsProps {
|
||||
suggestions: Array<AutocompleteSuggestion<InternalDocSearchHit>>;
|
||||
getMenuProps: GetMenuProps;
|
||||
getItemProps: GetItemProps<InternalDocSearchHit, React.MouseEvent>;
|
||||
interface ResultsProps
|
||||
extends AutocompleteApi<
|
||||
InternalDocSearchHit,
|
||||
React.FormEvent,
|
||||
React.MouseEvent,
|
||||
React.KeyboardEvent
|
||||
> {
|
||||
state: AutocompleteState<InternalDocSearchHit>;
|
||||
onItemClick(item: InternalDocSearchHit): void;
|
||||
onAction(search: InternalDocSearchHit): void;
|
||||
}
|
||||
|
||||
export function Results(props: ResultsProps) {
|
||||
return (
|
||||
<div className="DocSearch-Dropdown-Container">
|
||||
{props.suggestions.map(({ source, items }) => {
|
||||
{props.state.suggestions.map(({ source, items }) => {
|
||||
if (items.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const title = items[0].hierarchy.lvl0;
|
||||
|
||||
return (
|
||||
|
|
@ -39,49 +49,49 @@ export function Results(props: ResultsProps) {
|
|||
{...props.getItemProps({
|
||||
item,
|
||||
source,
|
||||
onClick() {
|
||||
props.onItemClick(item);
|
||||
},
|
||||
})}
|
||||
>
|
||||
<a href={item.url}>
|
||||
|
||||
<div className="DocSearch-Hit-Container">
|
||||
{item.__docsearch_parent && (
|
||||
<svg className="DocSearch-Hit-Tree">
|
||||
<g
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{item.__docsearch_parent !==
|
||||
{item.__docsearch_parent && (
|
||||
<svg className="DocSearch-Hit-Tree">
|
||||
<g
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
fill="none"
|
||||
fillRule="evenodd"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
{item.__docsearch_parent !==
|
||||
items[index + 1]?.__docsearch_parent ? (
|
||||
<path d="M8 8v22M26.5 30H8.3" />
|
||||
) : (
|
||||
<path d="M8 8v44M26.5 30H8.3" />
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
</g>
|
||||
</svg>
|
||||
)}
|
||||
|
||||
<div className="DocSearch-Hit-icon">
|
||||
<SourceIcon type={item.type} />
|
||||
</div>
|
||||
|
||||
{item.hierarchy[item.type] && item.type === 'lvl1' && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<span
|
||||
<Snippet
|
||||
className="DocSearch-Hit-title"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
item._snippetResult.hierarchy.lvl1.value,
|
||||
}}
|
||||
hit={item}
|
||||
attribute="hierarchy.lvl1"
|
||||
/>
|
||||
{item.content && (
|
||||
<span
|
||||
<Snippet
|
||||
className="DocSearch-Hit-path"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item._snippetResult.content.value,
|
||||
}}
|
||||
hit={item}
|
||||
attribute="content"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -94,38 +104,30 @@ export function Results(props: ResultsProps) {
|
|||
item.type === 'lvl5' ||
|
||||
item.type === 'lvl6') && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<span
|
||||
<Snippet
|
||||
className="DocSearch-Hit-title"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
item._snippetResult.hierarchy[item.type]
|
||||
.value,
|
||||
}}
|
||||
hit={item}
|
||||
attribute={`hierarchy.${item.type}`}
|
||||
/>
|
||||
<span
|
||||
<Snippet
|
||||
className="DocSearch-Hit-path"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
item._snippetResult.hierarchy.lvl1.value,
|
||||
}}
|
||||
hit={item}
|
||||
attribute="hierarchy.lvl1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{item.type === 'content' && (
|
||||
<div className="DocSearch-Hit-content-wrapper">
|
||||
<span
|
||||
<Snippet
|
||||
className="DocSearch-Hit-title"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: item._snippetResult.content.value,
|
||||
}}
|
||||
hit={item}
|
||||
attribute="content"
|
||||
/>
|
||||
<span
|
||||
<Snippet
|
||||
className="DocSearch-Hit-path"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html:
|
||||
item._snippetResult.hierarchy.lvl1.value,
|
||||
}}
|
||||
hit={item}
|
||||
attribute="hierarchy.lvl1"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,23 @@
|
|||
import React, { MutableRefObject } from 'react';
|
||||
import {
|
||||
GetFormProps,
|
||||
GetLabelProps,
|
||||
GetInputProps,
|
||||
AutocompleteApi,
|
||||
AutocompleteState,
|
||||
} from '@francoischalifour/autocomplete-core';
|
||||
|
||||
import { InternalDocSearchHit } from '../types';
|
||||
import { SearchIcon } from './SearchIcon';
|
||||
import { ResetIcon } from './ResetIcon';
|
||||
import { LoadingIcon } from './LoadingIcon';
|
||||
|
||||
interface SearchBoxProps {
|
||||
inputRef: MutableRefObject<HTMLInputElement | null>;
|
||||
query: string;
|
||||
getFormProps: GetFormProps<React.FormEvent>;
|
||||
getLabelProps: GetLabelProps;
|
||||
getInputProps: GetInputProps<
|
||||
React.ChangeEvent,
|
||||
interface SearchBoxProps
|
||||
extends AutocompleteApi<
|
||||
InternalDocSearchHit,
|
||||
React.FormEvent,
|
||||
React.MouseEvent,
|
||||
React.KeyboardEvent
|
||||
>;
|
||||
> {
|
||||
state: AutocompleteState<InternalDocSearchHit>;
|
||||
inputRef: MutableRefObject<HTMLInputElement | null>;
|
||||
onClose(): void;
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +58,7 @@ export function SearchBox(props: SearchBoxProps) {
|
|||
type="reset"
|
||||
title="Clear the query"
|
||||
className="DocSearch-Reset"
|
||||
hidden={!props.query}
|
||||
hidden={!props.state.query}
|
||||
onClick={onReset}
|
||||
>
|
||||
<ResetIcon />
|
||||
|
|
|
|||
32
src/Snippet.tsx
Normal file
32
src/Snippet.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { createElement } from 'react';
|
||||
|
||||
import { InternalDocSearchHit } from './types';
|
||||
|
||||
function getPropertyByPath(object: object, path: string): any {
|
||||
const parts = path.split('.');
|
||||
|
||||
return parts.reduce((current, key) => current && current[key], object);
|
||||
}
|
||||
|
||||
interface SnippetProps {
|
||||
[prop: string]: unknown;
|
||||
hit: InternalDocSearchHit;
|
||||
attribute: string;
|
||||
tagName?: string;
|
||||
}
|
||||
|
||||
export function Snippet({
|
||||
hit,
|
||||
attribute,
|
||||
tagName = 'span',
|
||||
...rest
|
||||
}: SnippetProps) {
|
||||
return createElement(tagName, {
|
||||
...rest,
|
||||
dangerouslySetInnerHTML: {
|
||||
__html:
|
||||
getPropertyByPath(hit, `_snippetResult.${attribute}.value`) ||
|
||||
hit[attribute],
|
||||
},
|
||||
});
|
||||
}
|
||||
80
src/recent-searches.ts
Normal file
80
src/recent-searches.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
import { DocSearchHit, RecentDocSearchHit } from './types';
|
||||
|
||||
function isLocalStorageSupported() {
|
||||
const key = '__TEST_KEY__';
|
||||
|
||||
try {
|
||||
localStorage.setItem(key, '');
|
||||
localStorage.removeItem(key);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function createStorage<TItem>() {
|
||||
if (isLocalStorageSupported() === false) {
|
||||
return {
|
||||
setItem() {},
|
||||
getItem() {
|
||||
return [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const STORAGE_KEY = '__AUTOCOMPLETE_RECENT_SEARCHES__';
|
||||
|
||||
return {
|
||||
setItem(item: TItem[]) {
|
||||
return window.localStorage.setItem(STORAGE_KEY, JSON.stringify(item));
|
||||
},
|
||||
getItem(): TItem[] {
|
||||
const item = window.localStorage.getItem(STORAGE_KEY);
|
||||
|
||||
return item ? JSON.parse(item) : [];
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createRecentSearches<TItem extends RecentDocSearchHit>() {
|
||||
const storage = createStorage<TItem>();
|
||||
let items = storage.getItem();
|
||||
|
||||
return {
|
||||
saveSearch(search: TItem) {
|
||||
const {
|
||||
_highlightResult,
|
||||
_snippetResult,
|
||||
...item
|
||||
} = (search as unknown) as DocSearchHit;
|
||||
|
||||
if (item.type === 'content') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const isQueryAlreadySaved = items.findIndex(
|
||||
x => x.objectID === item.objectID
|
||||
);
|
||||
|
||||
if (isQueryAlreadySaved > -1) {
|
||||
items.splice(isQueryAlreadySaved, 1);
|
||||
}
|
||||
|
||||
items.unshift(item as TItem);
|
||||
items = items.slice(0, 5);
|
||||
|
||||
storage.setItem(items);
|
||||
|
||||
return true;
|
||||
},
|
||||
deleteSearch(item: TItem) {
|
||||
items = items.filter(x => x.objectID !== item.objectID);
|
||||
|
||||
storage.setItem(items);
|
||||
},
|
||||
getSearches() {
|
||||
return items;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
|
@ -447,6 +447,15 @@ html[data-theme='dark'] {
|
|||
color: var(--docsearch-muted-color);
|
||||
}
|
||||
|
||||
.DocSearch-Hit-action-button {
|
||||
appearance: none;
|
||||
background: none;
|
||||
color: inherit;
|
||||
border: none;
|
||||
padding: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.DocSearch-Hit-content-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
|
|
|
|||
6
src/types/RecentDocSearchHit.ts
Normal file
6
src/types/RecentDocSearchHit.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import { DocSearchHit } from './DocSearchHit';
|
||||
|
||||
export type RecentDocSearchHit = Omit<
|
||||
DocSearchHit,
|
||||
'_highlightResult' | '_snippetResult'
|
||||
>;
|
||||
|
|
@ -1,2 +1,3 @@
|
|||
export * from './DocSearchHit';
|
||||
export * from './InternalDocSearchHit';
|
||||
export * from './RecentDocSearchHit';
|
||||
|
|
|
|||
Loading…
Reference in a new issue