Skip to content

Commit f9eda8d

Browse files
authored
feat: support external search (#556)
* feat: support external search * tweak: styles * tweak: styles * chore: preventDefault * tweak: disable external search * tweak: styles * fix: query on click * test: operator preview * tweak: input blur * tweak: type search * fix: index * fix: index
1 parent 6d8df7d commit f9eda8d

File tree

5 files changed

+272
-64
lines changed

5 files changed

+272
-64
lines changed

docs

Submodule docs updated 7167 files

locale/en/translation.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,10 @@
2929
"contactUs": "Contact Us",
3030
"searchDocs": "Search Docs",
3131
"playground": "Playground",
32-
"learningCenter": "Learning Center"
32+
"learningCenter": "Learning Center",
33+
"onsiteSearch": "Onsite Search",
34+
"googleSearch": "Google Search",
35+
"bingSearch": "Bing Search"
3336
},
3437
"footer": {
3538
"privacy": "Privacy Policy",

locale/zh/translation.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
"contactUs": "联系我们",
2727
"searchDocs": "搜索文档",
2828
"playground": "Playground",
29-
"learningCenter": "Learning center"
29+
"learningCenter": "Learning center",
30+
"onsiteSearch": "站内搜索",
31+
"googleSearch": "Google 搜索",
32+
"bingSearch": "Bing 搜索"
3033
},
3134
"footer": {
3235
"privacy": "隐私政策",

src/components/Search/index.tsx

Lines changed: 262 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@ import * as React from "react";
22
import { useI18next } from "gatsby-plugin-react-i18next";
33
import { useLocation } from "@reach/router";
44
import Box from "@mui/material/Box";
5-
import { useTheme } from "@mui/material/styles";
65
import TextField, { TextFieldProps } from "@mui/material/TextField";
76
import InputAdornment from "@mui/material/InputAdornment";
87
import IconButton from "@mui/material/IconButton";
98
import { styled } from "@mui/material/styles";
109

1110
import SearchIcon from "@mui/icons-material/Search";
11+
import { Card, MenuItem, Popper, PopperProps } from "@mui/material";
12+
import { Locale } from "shared/interface";
1213

1314
const 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+
3140
export 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

Comments
 (0)