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+ }
0 commit comments