Skip to content

Commit a7ee4b7

Browse files
Tweaking search to filter for different areas (so XPF doesn't appear in Avalonia)
1 parent e5c60fd commit a7ee4b7

File tree

4 files changed

+287
-0
lines changed

4 files changed

+287
-0
lines changed

docusaurus.config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -307,6 +307,7 @@ const config = {
307307
// Public API key: it is safe to commit it
308308
apiKey: '028e3dad834905a2a2c2a7ad9da9e666',
309309
indexName: 'avaloniaui_docs',
310+
contextualSearch: true,
310311
},
311312
}),
312313
};

src/theme/SearchBar/index.js

Lines changed: 234 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,234 @@
1+
import React, {useCallback, useMemo, useRef, useState} from 'react';
2+
import {createPortal} from 'react-dom';
3+
import {DocSearchButton, useDocSearchKeyboardEvents} from '@docsearch/react';
4+
import Head from '@docusaurus/Head';
5+
import Link from '@docusaurus/Link';
6+
import {useHistory} from '@docusaurus/router';
7+
import DefaultSearchBar from '@theme-original/SearchBar';
8+
import {
9+
isRegexpStringMatch,
10+
useSearchLinkCreator,
11+
} from '@docusaurus/theme-common';
12+
import {
13+
useAlgoliaContextualFacetFilters,
14+
useSearchResultUrlProcessor,
15+
} from '@docusaurus/theme-search-algolia/client';
16+
import Translate from '@docusaurus/Translate';
17+
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
18+
import translations from '@theme/SearchTranslations';
19+
let DocSearchModal = null;
20+
function importDocSearchModalIfNeeded() {
21+
if (DocSearchModal) {
22+
return Promise.resolve();
23+
}
24+
return Promise.all([
25+
import('@docsearch/react/modal'),
26+
import('@docsearch/react/style'),
27+
import('./styles.css'),
28+
]).then(([{DocSearchModal: Modal}]) => {
29+
DocSearchModal = Modal;
30+
});
31+
}
32+
function useNavigator({externalUrlRegex}) {
33+
const history = useHistory();
34+
const [navigator] = useState(() => {
35+
return {
36+
navigate(params) {
37+
// Algolia results could contain URL's from other domains which cannot
38+
// be served through history and should navigate with window.location
39+
if (isRegexpStringMatch(externalUrlRegex, params.itemUrl)) {
40+
window.location.href = params.itemUrl;
41+
} else {
42+
history.push(params.itemUrl);
43+
}
44+
},
45+
};
46+
});
47+
return navigator;
48+
}
49+
function useTransformSearchClient() {
50+
const {
51+
siteMetadata: {docusaurusVersion},
52+
} = useDocusaurusContext();
53+
return useCallback(
54+
(searchClient) => {
55+
searchClient.addAlgoliaAgent('docusaurus', docusaurusVersion);
56+
return searchClient;
57+
},
58+
[docusaurusVersion],
59+
);
60+
}
61+
function useTransformItems(props) {
62+
const processSearchResultUrl = useSearchResultUrlProcessor();
63+
const [transformItems] = useState(() => {
64+
return (items) =>
65+
props.transformItems
66+
? // Custom transformItems
67+
props.transformItems(items)
68+
: // Default transformItems
69+
items.map((item) => ({
70+
...item,
71+
url: processSearchResultUrl(item.url),
72+
}));
73+
});
74+
return transformItems;
75+
}
76+
function useResultsFooterComponent({closeModal}) {
77+
return useMemo(
78+
() =>
79+
({state}) =>
80+
<ResultsFooter state={state} onClose={closeModal} />,
81+
[closeModal],
82+
);
83+
}
84+
function Hit({hit, children}) {
85+
return <Link to={hit.url}>{children}</Link>;
86+
}
87+
function ResultsFooter({state, onClose}) {
88+
const createSearchLink = useSearchLinkCreator();
89+
return (
90+
<Link to={createSearchLink(state.query)} onClick={onClose}>
91+
<Translate
92+
id="theme.SearchBar.seeAll"
93+
values={{count: state.context.nbHits}}>
94+
{'See all {count} results'}
95+
</Translate>
96+
</Link>
97+
);
98+
}
99+
function useSearchParameters({contextualSearch, ...props}) {
100+
function mergeFacetFilters(f1, f2) {
101+
const normalize = (f) => (typeof f === 'string' ? [f] : f);
102+
return [...normalize(f1), ...normalize(f2)];
103+
}
104+
const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
105+
const configFacetFilters = props.searchParameters?.facetFilters ?? [];
106+
const facetFilters = contextualSearch
107+
? // Merge contextual search filters with config filters
108+
mergeFacetFilters(contextualSearchFacetFilters, configFacetFilters)
109+
: // ... or use config facetFilters
110+
configFacetFilters;
111+
// We let users override default searchParameters if they want to
112+
return {
113+
...props.searchParameters,
114+
facetFilters,
115+
};
116+
}
117+
function DocSearch({externalUrlRegex, ...props}) {
118+
const navigator = useNavigator({externalUrlRegex});
119+
const searchParameters = useSearchParameters({...props});
120+
const transformItems = useTransformItems(props);
121+
const transformSearchClient = useTransformSearchClient();
122+
const searchContainer = useRef(null);
123+
// TODO remove "as any" after React 19 upgrade
124+
const searchButtonRef = useRef(null);
125+
const [isOpen, setIsOpen] = useState(false);
126+
const [initialQuery, setInitialQuery] = useState(undefined);
127+
const prepareSearchContainer = useCallback(() => {
128+
if (!searchContainer.current) {
129+
const divElement = document.createElement('div');
130+
searchContainer.current = divElement;
131+
document.body.insertBefore(divElement, document.body.firstChild);
132+
}
133+
}, []);
134+
const openModal = useCallback(() => {
135+
prepareSearchContainer();
136+
importDocSearchModalIfNeeded().then(() => setIsOpen(true));
137+
}, [prepareSearchContainer]);
138+
const closeModal = useCallback(() => {
139+
setIsOpen(false);
140+
searchButtonRef.current?.focus();
141+
setInitialQuery(undefined);
142+
}, []);
143+
const handleInput = useCallback(
144+
(event) => {
145+
if (event.key === 'f' && (event.metaKey || event.ctrlKey)) {
146+
// ignore browser's ctrl+f
147+
return;
148+
}
149+
// prevents duplicate key insertion in the modal input
150+
event.preventDefault();
151+
setInitialQuery(event.key);
152+
openModal();
153+
},
154+
[openModal],
155+
);
156+
const resultsFooterComponent = useResultsFooterComponent({closeModal});
157+
useDocSearchKeyboardEvents({
158+
isOpen,
159+
onOpen: openModal,
160+
onClose: closeModal,
161+
onInput: handleInput,
162+
searchButtonRef,
163+
});
164+
return (
165+
<>
166+
<Head>
167+
{/* This hints the browser that the website will load data from Algolia,
168+
and allows it to preconnect to the DocSearch cluster. It makes the first
169+
query faster, especially on mobile. */}
170+
<link
171+
rel="preconnect"
172+
href={`https://${props.appId}-dsn.algolia.net`}
173+
crossOrigin="anonymous"
174+
/>
175+
</Head>
176+
177+
<DocSearchButton
178+
onTouchStart={importDocSearchModalIfNeeded}
179+
onFocus={importDocSearchModalIfNeeded}
180+
onMouseOver={importDocSearchModalIfNeeded}
181+
onClick={openModal}
182+
ref={searchButtonRef}
183+
translations={props.translations?.button ?? translations.button}
184+
/>
185+
186+
{isOpen &&
187+
DocSearchModal &&
188+
searchContainer.current &&
189+
createPortal(
190+
<DocSearchModal
191+
onClose={closeModal}
192+
initialScrollY={window.scrollY}
193+
initialQuery={initialQuery}
194+
navigator={navigator}
195+
transformItems={transformItems}
196+
hitComponent={Hit}
197+
transformSearchClient={transformSearchClient}
198+
{...(props.searchPagePath && {
199+
resultsFooterComponent,
200+
})}
201+
placeholder={translations.placeholder}
202+
{...props}
203+
translations={props.translations?.modal ?? translations.modal}
204+
searchParameters={searchParameters}
205+
/>,
206+
searchContainer.current,
207+
)}
208+
</>
209+
);
210+
}
211+
export default function SearchBar() {
212+
// Detect product from the current pathname
213+
const path = typeof window !== 'undefined' ? window.location.pathname : '';
214+
let product = null;
215+
216+
if (path.startsWith('/xpf/')) {
217+
product = 'xpf';
218+
} else if (path.startsWith('/docs/')) {
219+
product = 'avalonia';
220+
} else if (path.startsWith('/accelerate/')) {
221+
product = 'accelerate';
222+
}
223+
224+
const searchParameters = product ? { facetFilters: [`product:${product}`] } : {};
225+
226+
return (
227+
<DocSearch
228+
appId="V9UF6750GH"
229+
indexName="avaloniaui_docs"
230+
apiKey="028e3dad834905a2a2c2a7ad9da9e666"
231+
searchParameters={searchParameters}
232+
/>
233+
);
234+
}

src/theme/SearchBar/styles.css

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
:root {
2+
--docsearch-primary-color: var(--ifm-color-primary);
3+
--docsearch-text-color: var(--ifm-font-color-base);
4+
}
5+
6+
.DocSearch-Button {
7+
margin: 0;
8+
transition: all var(--ifm-transition-fast)
9+
var(--ifm-transition-timing-default);
10+
}
11+
12+
.DocSearch-Container {
13+
z-index: calc(var(--ifm-z-index-fixed) + 1);
14+
}

src/theme/SearchTranslations.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
export default {
2+
button: {
3+
buttonText: 'Search',
4+
buttonAriaLabel: 'Search',
5+
},
6+
modal: {
7+
searchBox: {
8+
resetButtonTitle: 'Clear the query',
9+
resetButtonAriaLabel: 'Clear the query',
10+
cancelButtonText: 'Cancel',
11+
cancelButtonAriaLabel: 'Cancel',
12+
},
13+
startScreen: {
14+
recentSearchesTitle: 'Recent',
15+
noRecentSearchesText: 'No recent searches',
16+
saveRecentSearchButtonTitle: 'Save to recent',
17+
removeRecentSearchButtonTitle: 'Remove from recent',
18+
favoriteSearchesTitle: 'Favourites',
19+
removeFavoriteSearchButtonTitle: 'Remove from favourites',
20+
},
21+
errorScreen: {
22+
titleText: 'Unable to fetch results',
23+
helpText: 'You might want to check your network connection.',
24+
},
25+
footer: {
26+
selectText: 'to select',
27+
navigateText: 'to navigate',
28+
closeText: 'to close',
29+
searchByText: 'Search by',
30+
},
31+
noResultsScreen: {
32+
noResultsText: 'No results for',
33+
suggestedQueryText: 'Try searching for',
34+
reportMissingResultsText: 'Think this query should return results?',
35+
reportMissingResultsLinkText: 'Let us know.',
36+
},
37+
},
38+
};

0 commit comments

Comments
 (0)