Skip to content

Commit

Permalink
feat: support external search (#556)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
shhdgit authored Nov 25, 2024
1 parent 6d8df7d commit f9eda8d
Show file tree
Hide file tree
Showing 5 changed files with 272 additions and 64 deletions.
2 changes: 1 addition & 1 deletion docs
Submodule docs updated 7167 files
5 changes: 4 additions & 1 deletion locale/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@
"contactUs": "Contact Us",
"searchDocs": "Search Docs",
"playground": "Playground",
"learningCenter": "Learning Center"
"learningCenter": "Learning Center",
"onsiteSearch": "Onsite Search",
"googleSearch": "Google Search",
"bingSearch": "Bing Search"
},
"footer": {
"privacy": "Privacy Policy",
Expand Down
5 changes: 4 additions & 1 deletion locale/zh/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,10 @@
"contactUs": "联系我们",
"searchDocs": "搜索文档",
"playground": "Playground",
"learningCenter": "Learning center"
"learningCenter": "Learning center",
"onsiteSearch": "站内搜索",
"googleSearch": "Google 搜索",
"bingSearch": "Bing 搜索"
},
"footer": {
"privacy": "隐私政策",
Expand Down
323 changes: 262 additions & 61 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import * as React from "react";
import { useI18next } from "gatsby-plugin-react-i18next";
import { useLocation } from "@reach/router";
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import TextField, { TextFieldProps } from "@mui/material/TextField";
import InputAdornment from "@mui/material/InputAdornment";
import IconButton from "@mui/material/IconButton";
import { styled } from "@mui/material/styles";

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

const StyledTextField = styled((props: TextFieldProps) => (
<TextField {...props} />
Expand All @@ -28,37 +29,77 @@ const StyledTextField = styled((props: TextFieldProps) => (
},
}));

const SEARCH_WIDTH = 251;

enum SearchType {
Onsite = "onsite",
Google = "google",
Bing = "bing",
}

export default function Search(props: {
placeholder?: string;
disableResponsive?: boolean;
disableExternalSearch?: boolean;
docInfo: { type: string; version: string };
}) {
const { placeholder, disableResponsive, docInfo } = props;
const { placeholder, disableResponsive, docInfo, disableExternalSearch } =
props;

const anchorEl = React.useRef<HTMLDivElement>(null);
const inputEl = React.useRef<HTMLInputElement>(null);
const [queryStr, setQueryStr] = React.useState("");
const [isFocus, setIsFocus] = React.useState(false);
const [popperItemIndex, setPopperItemIndex] = React.useState(0);
const searchTypeRef = React.useRef<string>(SearchType.Onsite);

const { t, navigate } = useI18next();
const theme = useTheme();
const { t, navigate, language } = useI18next();
const location = useLocation();

const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setQueryStr(event.target.value);
};

const handleSearchSubmitCallback = React.useCallback(() => {
navigate(
`/search?type=${docInfo.type}&version=${
docInfo.version
}&q=${encodeURIComponent(queryStr)}`,
{
state: {
type: docInfo.type,
version: docInfo.version,
query: queryStr,
},
}
);
}, [docInfo, queryStr]);
const handleSearchSubmitCallback = (query: string, forceType?: string) => {
const searchType = forceType || searchTypeRef.current;
const q = encodeURIComponent(query);

inputEl.current?.blur();

if (searchType === SearchType.Onsite) {
navigate(
`/search?type=${docInfo.type}&version=${docInfo.version}&q=${q}`,
{
state: {
type: docInfo.type,
version: docInfo.version,
query: query,
},
}
);
return;
}

const segmentPath = `${language === Locale.en ? "" : `${language}/`}${
docInfo.type
}`;

if (searchType === SearchType.Google) {
window.open(
`https://www.google.com/search?q=site%3Adocs.pingcap.com/${segmentPath}+${q}`,
"_blank"
);
return;
}

if (searchType === SearchType.Bing) {
window.open(
`https://cn.bing.com/search?q=site%3Adocs.pingcap.com/${segmentPath}+${q}`,
"_blank"
);
return;
}
};

React.useEffect(() => {
const searchParams = new URLSearchParams(location.search);
Expand All @@ -67,56 +108,216 @@ export default function Search(props: {
}, [location.search]);

return (
<Box>
{!disableResponsive && (
<IconButton
<>
<Box ref={anchorEl}>
{!disableResponsive && (
<IconButton
sx={{
display: {
lg: "none",
},
}}
onClick={() => handleSearchSubmitCallback(queryStr)}
>
<SearchIcon />
</IconButton>
)}
<Box
component="form"
noValidate
autoComplete="off"
sx={{
width: SEARCH_WIDTH,
display: {
lg: "none",
xs: disableResponsive ? "block" : "none",
lg: "block",
},
}}
onClick={handleSearchSubmitCallback}
>
<SearchIcon />
</IconButton>
)}
<Box
component="form"
noValidate
autoComplete="off"
sx={{
width: "251px",
display: {
xs: disableResponsive ? "block" : "none",
lg: "block",
<StyledTextField
inputRef={inputEl}
size="small"
id="doc-search"
fullWidth
placeholder={t("navbar.searchDocs") || placeholder}
type="search"
variant="outlined"
value={queryStr}
onChange={handleChange}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleSearchSubmitCallback(queryStr);
}
if (e.key === "ArrowUp") {
e.preventDefault();
setPopperItemIndex((i) => --i);
}
if (e.key === "ArrowDown") {
e.preventDefault();
setPopperItemIndex((i) => ++i);
}
}}
onSubmit={() => handleSearchSubmitCallback(queryStr)}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
}}
onFocus={() => setIsFocus(true)}
onBlur={() => setTimeout(() => setIsFocus(false), 100)}
/>
</Box>
</Box>
<SearchPopper
open={!!queryStr && isFocus && !disableExternalSearch}
query={queryStr}
anchorEl={anchorEl.current}
popperItemIndex={popperItemIndex}
onUpdateIndex={setPopperItemIndex}
onUpdateSearchType={(type) => (searchTypeRef.current = type)}
onClickItem={handleSearchSubmitCallback}
/>
</>
);
}

interface SearchPopperItemProps {
type: SearchType;
component: (props: {
selected: boolean;
query: string;
}) => React.ReactElement;
}

const SearchPopper = ({
open,
anchorEl,
popperItemIndex,
query,
onUpdateIndex,
onUpdateSearchType,
onClickItem,
}: PopperProps & {
query: string;
popperItemIndex: number;
onUpdateIndex: (index: number) => void;
onUpdateSearchType: (type: SearchType) => void;
onClickItem: (query: string, type: SearchType) => void;
}) => {
const { t, language } = useI18next();
const items: SearchPopperItemProps[] = React.useMemo(
() =>
(
[
{
type: SearchType.Onsite,
component: ({ selected, query }) => (
<SearchPopperMenuItem
name={t("navbar.onsiteSearch")}
selected={selected}
query={query}
onClick={() => onClickItem(query, SearchType.Onsite)}
/>
),
},
{
type: SearchType.Google,
component: ({ selected, query }) => (
<SearchPopperMenuItem
name={t("navbar.googleSearch")}
selected={selected}
query={query}
onClick={() => onClickItem(query, SearchType.Google)}
/>
),
},
{
type: SearchType.Bing,
component: ({ selected, query }) => (
<SearchPopperMenuItem
name={t("navbar.bingSearch")}
selected={selected}
query={query}
onClick={() => onClickItem(query, SearchType.Bing)}
/>
),
},
] as SearchPopperItemProps[]
).filter((item) =>
language === Locale.zh
? item.type !== SearchType.Google
: item.type !== SearchType.Bing
),
[]
);
const currentIndex =
(popperItemIndex < 0 ? items.length - popperItemIndex : popperItemIndex) %
items.length;

React.useEffect(() => {
onUpdateSearchType(items[currentIndex].type);
}, [currentIndex]);

return (
<Popper
open={open}
anchorEl={anchorEl}
sx={{ zIndex: 99 }}
modifiers={[{ name: "offset", options: { offset: [0, 8] } }]}
>
<Card
sx={{
width: SEARCH_WIDTH,
wordBreak: "break-all",
padding: "8px",
boxSizing: "border-box",
}}
>
<StyledTextField
size="small"
id="doc-search"
fullWidth
placeholder={t("navbar.searchDocs") || placeholder}
type="search"
variant="outlined"
value={queryStr}
onChange={handleChange}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
handleSearchSubmitCallback();
}
}}
onSubmit={handleSearchSubmitCallback}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon fontSize="small" />
</InputAdornment>
),
{items.map((item, index) => (
<Box onMouseEnter={() => onUpdateIndex(index)} key={item.type}>
<item.component query={query} selected={currentIndex === index} />
</Box>
))}
</Card>
</Popper>
);
};

const SearchPopperMenuItem = ({
name,
selected,
query,
onClick,
}: {
name: string;
selected: boolean;
query: string;
onClick: () => void;
}) => {
return (
<MenuItem
selected={selected}
onClick={onClick}
sx={{
textWrap: "auto",
padding: "6px 10px",
}}
>
<span>
<span
style={{
fontSize: "14px",
paddingRight: "6px",
color: "#807c7c",
}}
/>
</Box>
</Box>
>
{name}:
</span>
<span>{query}</span>
</span>
</MenuItem>
);
}
};
Loading

0 comments on commit f9eda8d

Please sign in to comment.