@@ -2,13 +2,14 @@ import * as React from "react";
22import { useI18next } from "gatsby-plugin-react-i18next" ;
33import { useLocation } from "@reach/router" ;
44import Box from "@mui/material/Box" ;
5- import { useTheme } from "@mui/material/styles" ;
65import TextField , { TextFieldProps } from "@mui/material/TextField" ;
76import InputAdornment from "@mui/material/InputAdornment" ;
87import IconButton from "@mui/material/IconButton" ;
98import { styled } from "@mui/material/styles" ;
109
1110import SearchIcon from "@mui/icons-material/Search" ;
11+ import { Card , MenuItem , Popper , PopperProps } from "@mui/material" ;
12+ import { Locale } from "shared/interface" ;
1213
1314const StyledTextField = styled ( ( props : TextFieldProps ) => (
1415 < TextField { ...props } />
@@ -28,37 +29,77 @@ const StyledTextField = styled((props: TextFieldProps) => (
2829 } ,
2930} ) ) ;
3031
32+ const SEARCH_WIDTH = 251 ;
33+
34+ enum SearchType {
35+ Onsite = "onsite" ,
36+ Google = "google" ,
37+ Bing = "bing" ,
38+ }
39+
3140export default function Search ( props : {
3241 placeholder ?: string ;
3342 disableResponsive ?: boolean ;
43+ disableExternalSearch ?: boolean ;
3444 docInfo : { type : string ; version : string } ;
3545} ) {
36- const { placeholder, disableResponsive, docInfo } = props ;
46+ const { placeholder, disableResponsive, docInfo, disableExternalSearch } =
47+ props ;
3748
49+ const anchorEl = React . useRef < HTMLDivElement > ( null ) ;
50+ const inputEl = React . useRef < HTMLInputElement > ( null ) ;
3851 const [ queryStr , setQueryStr ] = React . useState ( "" ) ;
52+ const [ isFocus , setIsFocus ] = React . useState ( false ) ;
53+ const [ popperItemIndex , setPopperItemIndex ] = React . useState ( 0 ) ;
54+ const searchTypeRef = React . useRef < string > ( SearchType . Onsite ) ;
3955
40- const { t, navigate } = useI18next ( ) ;
41- const theme = useTheme ( ) ;
56+ const { t, navigate, language } = useI18next ( ) ;
4257 const location = useLocation ( ) ;
4358
4459 const handleChange = ( event : React . ChangeEvent < HTMLInputElement > ) => {
4560 setQueryStr ( event . target . value ) ;
4661 } ;
4762
48- const handleSearchSubmitCallback = React . useCallback ( ( ) => {
49- navigate (
50- `/search?type=${ docInfo . type } &version=${
51- docInfo . version
52- } &q=${ encodeURIComponent ( queryStr ) } `,
53- {
54- state : {
55- type : docInfo . type ,
56- version : docInfo . version ,
57- query : queryStr ,
58- } ,
59- }
60- ) ;
61- } , [ docInfo , queryStr ] ) ;
63+ const handleSearchSubmitCallback = ( query : string , forceType ?: string ) => {
64+ const searchType = forceType || searchTypeRef . current ;
65+ const q = encodeURIComponent ( query ) ;
66+
67+ inputEl . current ?. blur ( ) ;
68+
69+ if ( searchType === SearchType . Onsite ) {
70+ navigate (
71+ `/search?type=${ docInfo . type } &version=${ docInfo . version } &q=${ q } ` ,
72+ {
73+ state : {
74+ type : docInfo . type ,
75+ version : docInfo . version ,
76+ query : query ,
77+ } ,
78+ }
79+ ) ;
80+ return ;
81+ }
82+
83+ const segmentPath = `${ language === Locale . en ? "" : `${ language } /` } ${
84+ docInfo . type
85+ } `;
86+
87+ if ( searchType === SearchType . Google ) {
88+ window . open (
89+ `https://www.google.com/search?q=site%3Adocs.pingcap.com/${ segmentPath } +${ q } ` ,
90+ "_blank"
91+ ) ;
92+ return ;
93+ }
94+
95+ if ( searchType === SearchType . Bing ) {
96+ window . open (
97+ `https://cn.bing.com/search?q=site%3Adocs.pingcap.com/${ segmentPath } +${ q } ` ,
98+ "_blank"
99+ ) ;
100+ return ;
101+ }
102+ } ;
62103
63104 React . useEffect ( ( ) => {
64105 const searchParams = new URLSearchParams ( location . search ) ;
@@ -67,56 +108,216 @@ export default function Search(props: {
67108 } , [ location . search ] ) ;
68109
69110 return (
70- < Box >
71- { ! disableResponsive && (
72- < IconButton
111+ < >
112+ < Box ref = { anchorEl } >
113+ { ! disableResponsive && (
114+ < IconButton
115+ sx = { {
116+ display : {
117+ lg : "none" ,
118+ } ,
119+ } }
120+ onClick = { ( ) => handleSearchSubmitCallback ( queryStr ) }
121+ >
122+ < SearchIcon />
123+ </ IconButton >
124+ ) }
125+ < Box
126+ component = "form"
127+ noValidate
128+ autoComplete = "off"
73129 sx = { {
130+ width : SEARCH_WIDTH ,
74131 display : {
75- lg : "none" ,
132+ xs : disableResponsive ? "block" : "none" ,
133+ lg : "block" ,
76134 } ,
77135 } }
78- onClick = { handleSearchSubmitCallback }
79136 >
80- < SearchIcon />
81- </ IconButton >
82- ) }
83- < Box
84- component = "form"
85- noValidate
86- autoComplete = "off"
87- sx = { {
88- width : "251px" ,
89- display : {
90- xs : disableResponsive ? "block" : "none" ,
91- lg : "block" ,
137+ < StyledTextField
138+ inputRef = { inputEl }
139+ size = "small"
140+ id = "doc-search"
141+ fullWidth
142+ placeholder = { t ( "navbar.searchDocs" ) || placeholder }
143+ type = "search"
144+ variant = "outlined"
145+ value = { queryStr }
146+ onChange = { handleChange }
147+ onKeyDown = { ( e ) => {
148+ if ( e . key === "Enter" ) {
149+ e . preventDefault ( ) ;
150+ handleSearchSubmitCallback ( queryStr ) ;
151+ }
152+ if ( e . key === "ArrowUp" ) {
153+ e . preventDefault ( ) ;
154+ setPopperItemIndex ( ( i ) => -- i ) ;
155+ }
156+ if ( e . key === "ArrowDown" ) {
157+ e . preventDefault ( ) ;
158+ setPopperItemIndex ( ( i ) => ++ i ) ;
159+ }
160+ } }
161+ onSubmit = { ( ) => handleSearchSubmitCallback ( queryStr ) }
162+ InputProps = { {
163+ startAdornment : (
164+ < InputAdornment position = "start" >
165+ < SearchIcon fontSize = "small" />
166+ </ InputAdornment >
167+ ) ,
168+ } }
169+ onFocus = { ( ) => setIsFocus ( true ) }
170+ onBlur = { ( ) => setTimeout ( ( ) => setIsFocus ( false ) , 100 ) }
171+ />
172+ </ Box >
173+ </ Box >
174+ < SearchPopper
175+ open = { ! ! queryStr && isFocus && ! disableExternalSearch }
176+ query = { queryStr }
177+ anchorEl = { anchorEl . current }
178+ popperItemIndex = { popperItemIndex }
179+ onUpdateIndex = { setPopperItemIndex }
180+ onUpdateSearchType = { ( type ) => ( searchTypeRef . current = type ) }
181+ onClickItem = { handleSearchSubmitCallback }
182+ />
183+ </ >
184+ ) ;
185+ }
186+
187+ interface SearchPopperItemProps {
188+ type : SearchType ;
189+ component : ( props : {
190+ selected : boolean ;
191+ query : string ;
192+ } ) => React . ReactElement ;
193+ }
194+
195+ const SearchPopper = ( {
196+ open,
197+ anchorEl,
198+ popperItemIndex,
199+ query,
200+ onUpdateIndex,
201+ onUpdateSearchType,
202+ onClickItem,
203+ } : PopperProps & {
204+ query : string ;
205+ popperItemIndex : number ;
206+ onUpdateIndex : ( index : number ) => void ;
207+ onUpdateSearchType : ( type : SearchType ) => void ;
208+ onClickItem : ( query : string , type : SearchType ) => void ;
209+ } ) => {
210+ const { t, language } = useI18next ( ) ;
211+ const items : SearchPopperItemProps [ ] = React . useMemo (
212+ ( ) =>
213+ (
214+ [
215+ {
216+ type : SearchType . Onsite ,
217+ component : ( { selected, query } ) => (
218+ < SearchPopperMenuItem
219+ name = { t ( "navbar.onsiteSearch" ) }
220+ selected = { selected }
221+ query = { query }
222+ onClick = { ( ) => onClickItem ( query , SearchType . Onsite ) }
223+ />
224+ ) ,
92225 } ,
226+ {
227+ type : SearchType . Google ,
228+ component : ( { selected, query } ) => (
229+ < SearchPopperMenuItem
230+ name = { t ( "navbar.googleSearch" ) }
231+ selected = { selected }
232+ query = { query }
233+ onClick = { ( ) => onClickItem ( query , SearchType . Google ) }
234+ />
235+ ) ,
236+ } ,
237+ {
238+ type : SearchType . Bing ,
239+ component : ( { selected, query } ) => (
240+ < SearchPopperMenuItem
241+ name = { t ( "navbar.bingSearch" ) }
242+ selected = { selected }
243+ query = { query }
244+ onClick = { ( ) => onClickItem ( query , SearchType . Bing ) }
245+ />
246+ ) ,
247+ } ,
248+ ] as SearchPopperItemProps [ ]
249+ ) . filter ( ( item ) =>
250+ language === Locale . zh
251+ ? item . type !== SearchType . Google
252+ : item . type !== SearchType . Bing
253+ ) ,
254+ [ ]
255+ ) ;
256+ const currentIndex =
257+ ( popperItemIndex < 0 ? items . length - popperItemIndex : popperItemIndex ) %
258+ items . length ;
259+
260+ React . useEffect ( ( ) => {
261+ onUpdateSearchType ( items [ currentIndex ] . type ) ;
262+ } , [ currentIndex ] ) ;
263+
264+ return (
265+ < Popper
266+ open = { open }
267+ anchorEl = { anchorEl }
268+ sx = { { zIndex : 99 } }
269+ modifiers = { [ { name : "offset" , options : { offset : [ 0 , 8 ] } } ] }
270+ >
271+ < Card
272+ sx = { {
273+ width : SEARCH_WIDTH ,
274+ wordBreak : "break-all" ,
275+ padding : "8px" ,
276+ boxSizing : "border-box" ,
93277 } }
94278 >
95- < StyledTextField
96- size = "small"
97- id = "doc-search"
98- fullWidth
99- placeholder = { t ( "navbar.searchDocs" ) || placeholder }
100- type = "search"
101- variant = "outlined"
102- value = { queryStr }
103- onChange = { handleChange }
104- onKeyDown = { ( e ) => {
105- if ( e . key === "Enter" ) {
106- e . preventDefault ( ) ;
107- handleSearchSubmitCallback ( ) ;
108- }
109- } }
110- onSubmit = { handleSearchSubmitCallback }
111- InputProps = { {
112- startAdornment : (
113- < InputAdornment position = "start" >
114- < SearchIcon fontSize = "small" />
115- </ InputAdornment >
116- ) ,
279+ { items . map ( ( item , index ) => (
280+ < Box onMouseEnter = { ( ) => onUpdateIndex ( index ) } key = { item . type } >
281+ < item . component query = { query } selected = { currentIndex === index } />
282+ </ Box >
283+ ) ) }
284+ </ Card >
285+ </ Popper >
286+ ) ;
287+ } ;
288+
289+ const SearchPopperMenuItem = ( {
290+ name,
291+ selected,
292+ query,
293+ onClick,
294+ } : {
295+ name : string ;
296+ selected : boolean ;
297+ query : string ;
298+ onClick : ( ) => void ;
299+ } ) => {
300+ return (
301+ < MenuItem
302+ selected = { selected }
303+ onClick = { onClick }
304+ sx = { {
305+ textWrap : "auto" ,
306+ padding : "6px 10px" ,
307+ } }
308+ >
309+ < span >
310+ < span
311+ style = { {
312+ fontSize : "14px" ,
313+ paddingRight : "6px" ,
314+ color : "#807c7c" ,
117315 } }
118- />
119- </ Box >
120- </ Box >
316+ >
317+ { name } :
318+ </ span >
319+ < span > { query } </ span >
320+ </ span >
321+ </ MenuItem >
121322 ) ;
122- }
323+ } ;
0 commit comments