mirror of
https://gitee.com/ant-design/ant-design.git
synced 2024-11-29 18:50:00 +08:00
parent
fbcbf4d522
commit
0d619766e0
@ -154,6 +154,7 @@
|
||||
"@ant-design/bisheng-plugin": "^2.3.0",
|
||||
"@ant-design/hitu": "^0.0.0-alpha.13",
|
||||
"@ant-design/tools": "^13.4.1-beta.0",
|
||||
"@docsearch/react": "^3.0.0-alpha.36",
|
||||
"@qixian.cs/github-contributors-list": "^1.0.3",
|
||||
"@stackblitz/sdk": "^1.3.0",
|
||||
"@types/classnames": "^2.2.8",
|
||||
|
@ -1,3 +1,5 @@
|
||||
@import '../template/Layout/Header/DocSearch.less';
|
||||
|
||||
[data-theme='dark'] {
|
||||
/* Change autocomplete styles in WebKit */
|
||||
input:-webkit-autofill,
|
||||
@ -366,4 +368,22 @@
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
}
|
||||
}
|
||||
|
||||
// customization DocSearch Modal component in dark mode
|
||||
#antd_algolia_search_modal {
|
||||
.customDocSearch();
|
||||
|
||||
// additional style reset in dark mode
|
||||
--docsearch-key-gradient: @background-color-light;
|
||||
--docsearch-logo-color: @white;
|
||||
|
||||
.DocSearch-Modal,
|
||||
.DocSearch-Commands-Key {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.DocSearch-Hit a {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,7 @@
|
||||
}
|
||||
|
||||
.algolia-docsearch-suggestion--highlight {
|
||||
padding: 0;
|
||||
color: @primary-color;
|
||||
}
|
||||
}
|
||||
|
32
site/theme/template/Layout/Header/DocSearch.less
Normal file
32
site/theme/template/Layout/Header/DocSearch.less
Normal file
@ -0,0 +1,32 @@
|
||||
// theme customization for DocSearch Modal
|
||||
.customDocSearch() {
|
||||
.DocSearch {
|
||||
--docsearch-primary-color: @primary-color;
|
||||
--docsearch-text-color: @text-color;
|
||||
--docsearch-muted-color: @text-color-secondary;
|
||||
--docsearch-container-background: @modal-mask-bg;
|
||||
--docsearch-searchbox-shadow: inset 0 0 0 2px @primary-color;
|
||||
|
||||
// Modal
|
||||
--docsearch-modal-background: @modal-content-bg;
|
||||
|
||||
// Search box
|
||||
--docsearch-searchbox-background: @input-bg;
|
||||
--docsearch-searchbox-focus-background: @input-bg;
|
||||
|
||||
// Footer
|
||||
--docsearch-footer-background: @modal-content-bg;
|
||||
|
||||
// Hits
|
||||
--docsearch-hit-background: @modal-content-bg;
|
||||
--docsearch-hit-color: @primary-color;
|
||||
--docsearch-hit-active-color: @text-color-inverse;
|
||||
--docsearch-highlight-color: @primary-color;
|
||||
|
||||
// apply antd modal style
|
||||
.DocSearch-Footer {
|
||||
border-top: @modal-footer-border-width @modal-footer-border-style
|
||||
@modal-footer-border-color-split;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
@import '../../../static/theme.less';
|
||||
@import '../../../../../components/style/themes/default.less';
|
||||
@import './index.less';
|
||||
@import './DocSearch.less';
|
||||
|
||||
@search-icon-color: #ced4d9;
|
||||
|
||||
@ -17,6 +18,23 @@
|
||||
border-left: 1px solid @site-border-color-split;
|
||||
transition: width 0.5s;
|
||||
|
||||
.keybindings {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.keybinding {
|
||||
color: @search-icon-color;
|
||||
kbd {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
padding: 0;
|
||||
// better keybinding font display using `Arial`
|
||||
font-family: Arial; /* stylelint-disable-line font-family-no-missing-generic-family-keyword */
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-row-rtl & {
|
||||
margin: 0 0 0 auto !important;
|
||||
padding-right: 16px;
|
||||
@ -39,6 +57,12 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper {
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
max-width: 200px;
|
||||
@ -136,3 +160,28 @@
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.DocSearch {
|
||||
font-family: @font-family;
|
||||
}
|
||||
|
||||
#antd_algolia_search_modal {
|
||||
// avoid the veritcal scrollbar(looks like a white block)
|
||||
.DocSearch-Hit-title {
|
||||
overflow-y: hidden;
|
||||
|
||||
mark {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// reset original `box-shadow` style
|
||||
.DocSearch-Footer {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
// customization DocSearch Modal component in light mode
|
||||
body[data-theme='default'] #antd_algolia_search_modal {
|
||||
.customDocSearch();
|
||||
}
|
196
site/theme/template/Layout/Header/SearchBar.tsx
Normal file
196
site/theme/template/Layout/Header/SearchBar.tsx
Normal file
@ -0,0 +1,196 @@
|
||||
import * as React from 'react';
|
||||
import * as ReactDOM from 'react-dom';
|
||||
import { Link, browserHistory } from 'bisheng/router';
|
||||
import classNames from 'classnames';
|
||||
import { Helmet } from 'react-helmet-async';
|
||||
import canUseDom from 'rc-util/lib/Dom/canUseDom';
|
||||
import { Input, Tooltip, Typography } from 'antd';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { DocSearchProps, useDocSearchKeyboardEvents, DocSearchModalProps } from '@docsearch/react';
|
||||
import '@docsearch/css';
|
||||
|
||||
import { SharedProps } from './interface';
|
||||
import { IAlgoliaConfig, transformHitUrl } from './algolia-config';
|
||||
|
||||
import './SearchBar.less';
|
||||
|
||||
const { Text } = Typography;
|
||||
|
||||
export interface SearchBarProps extends SharedProps {
|
||||
onTriggerFocus?: (focus: boolean) => void;
|
||||
responsive: null | 'narrow' | 'crowded';
|
||||
algoliaConfig: IAlgoliaConfig;
|
||||
}
|
||||
|
||||
let SearchModal: React.FC<DocSearchModalProps> | null = null;
|
||||
|
||||
const Hit: DocSearchProps['hitComponent'] = ({ hit, children }) => {
|
||||
const toUrl = React.useMemo(() => transformHitUrl(hit.url), [hit.url]);
|
||||
return <Link to={toUrl}>{children}</Link>;
|
||||
};
|
||||
|
||||
const CTRL_KEY = 'Ctrl';
|
||||
const CMD_KEY = '⌘';
|
||||
|
||||
function isAppleDevice() {
|
||||
return /(mac|iphone|ipod|ipad)/i.test(navigator.platform);
|
||||
}
|
||||
|
||||
/**
|
||||
* Recompose for algolia DocSearch Component Inspiring by
|
||||
*
|
||||
* - [@docusaurus-theme-search-algolia](https://docusaurus.io/docs/api/themes/@docusaurus/theme-search-algolia)
|
||||
* - [DocSearchModal Docs](https://autocomplete-experimental.netlify.app/docs/DocSearchModal)
|
||||
*/
|
||||
export const SearchBar = ({
|
||||
isZhCN,
|
||||
responsive,
|
||||
onTriggerFocus,
|
||||
algoliaConfig,
|
||||
}: SearchBarProps) => {
|
||||
const [isInputFocus, setInputFocus] = React.useState(false);
|
||||
const [inputSearch, setInputSearch] = React.useState('');
|
||||
|
||||
const [isModalOpen, setModalOpen] = React.useState(false);
|
||||
const [searchModalQuery, setSearchModalQuery] = React.useState('');
|
||||
const searchPlaceholder = isZhCN ? '在 ant.design 中搜索' : 'Search in ant.design';
|
||||
const searchInputPlaceholder = isZhCN ? '搜索' : 'Search';
|
||||
|
||||
const triggerSearchModalImport = React.useCallback(() => {
|
||||
if (SearchModal) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return import('@docsearch/react/modal').then(({ DocSearchModal }) => {
|
||||
SearchModal = DocSearchModal;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleInputFocus = React.useCallback((focus: boolean) => {
|
||||
setInputFocus(focus);
|
||||
onTriggerFocus?.(focus);
|
||||
}, []);
|
||||
|
||||
const handleInputChange = React.useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
triggerSearchModalImport();
|
||||
setInputSearch(event.target.value);
|
||||
}, []);
|
||||
|
||||
const searchModalContainer = React.useMemo(() => {
|
||||
if (!canUseDom()) {
|
||||
return;
|
||||
}
|
||||
const id = 'antd_algolia_search_modal';
|
||||
let searchModalContainer$ = document.querySelector(`#${id}`);
|
||||
if (!searchModalContainer$) {
|
||||
const containerDiv = document.createElement('div');
|
||||
containerDiv.id = id;
|
||||
document.body.appendChild(containerDiv);
|
||||
searchModalContainer$ = containerDiv;
|
||||
}
|
||||
return searchModalContainer$;
|
||||
}, []);
|
||||
|
||||
const handleModalOpen = React.useCallback(() => {
|
||||
triggerSearchModalImport().then(() => {
|
||||
handleInputFocus(true);
|
||||
setModalOpen(true);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleModalClose = React.useCallback(() => {
|
||||
// clear search value in SearchModal
|
||||
setSearchModalQuery('');
|
||||
setModalOpen(false);
|
||||
}, []);
|
||||
|
||||
useDocSearchKeyboardEvents({
|
||||
isOpen: isModalOpen,
|
||||
onOpen: handleModalOpen,
|
||||
onClose: handleModalClose,
|
||||
});
|
||||
|
||||
const searchParameters = React.useMemo(() => algoliaConfig.getSearchParams(isZhCN), [isZhCN]);
|
||||
|
||||
const navigator = React.useRef({
|
||||
navigate({ itemUrl }: { itemUrl: string }) {
|
||||
browserHistory.push(itemUrl);
|
||||
},
|
||||
}).current;
|
||||
|
||||
return (
|
||||
<div
|
||||
id="search-box"
|
||||
className={classNames({
|
||||
'narrow-mode': responsive,
|
||||
focused: isInputFocus,
|
||||
})}
|
||||
>
|
||||
<Helmet>
|
||||
{/* pre-connect to algolia server */}
|
||||
<link
|
||||
rel="preconnect"
|
||||
href={`https://${algoliaConfig.appId}-dsn.algolia.net`}
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
</Helmet>
|
||||
|
||||
<Input
|
||||
placeholder={searchInputPlaceholder}
|
||||
onTouchStart={triggerSearchModalImport}
|
||||
onMouseOver={triggerSearchModalImport}
|
||||
value={inputSearch}
|
||||
onChange={handleInputChange}
|
||||
onFocus={() => {
|
||||
triggerSearchModalImport();
|
||||
handleInputFocus(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
handleInputFocus(false);
|
||||
}}
|
||||
prefix={<SearchOutlined />}
|
||||
suffix={
|
||||
typeof window !== 'undefined' && (
|
||||
<Tooltip placement="right" title={isZhCN ? '唤起搜索窗' : 'Search in doc modal'}>
|
||||
<span
|
||||
className="keybindings"
|
||||
onClick={() => {
|
||||
// move userSearch to SearchModal
|
||||
setSearchModalQuery(inputSearch);
|
||||
setInputSearch('');
|
||||
handleModalOpen();
|
||||
}}
|
||||
>
|
||||
<Text keyboard className="keybinding">
|
||||
{isAppleDevice() ? CMD_KEY : CTRL_KEY}
|
||||
</Text>
|
||||
<Text keyboard className="keybinding">
|
||||
K
|
||||
</Text>
|
||||
</span>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
{SearchModal &&
|
||||
searchModalContainer &&
|
||||
isModalOpen &&
|
||||
ReactDOM.createPortal(
|
||||
<SearchModal
|
||||
navigator={navigator}
|
||||
onClose={handleModalClose}
|
||||
initialScrollY={window.scrollY}
|
||||
initialQuery={searchModalQuery}
|
||||
placeholder={searchPlaceholder}
|
||||
hitComponent={Hit}
|
||||
apiKey={algoliaConfig.apiKey}
|
||||
indexName={algoliaConfig.indexName}
|
||||
transformItems={algoliaConfig.transformData}
|
||||
searchParameters={searchParameters}
|
||||
/>,
|
||||
searchModalContainer,
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,62 +0,0 @@
|
||||
import * as React from 'react';
|
||||
import { Input } from 'antd';
|
||||
import classNames from 'classnames';
|
||||
import { SearchOutlined } from '@ant-design/icons';
|
||||
import { SharedProps } from './interface';
|
||||
|
||||
import './SearchBox.less';
|
||||
|
||||
export interface SearchBoxProps extends SharedProps {
|
||||
onTriggerFocus?: (focus: boolean) => void;
|
||||
responsive: null | 'narrow' | 'crowded';
|
||||
}
|
||||
|
||||
export default ({ isZhCN, responsive, onTriggerFocus }: SearchBoxProps) => {
|
||||
const inputRef = React.useRef<any>(null);
|
||||
const [focused, setFocused] = React.useState(false);
|
||||
const searchPlaceholder = isZhCN ? '在 ant.design 中搜索' : 'Search in ant.design';
|
||||
|
||||
function triggerFocus(focus: boolean) {
|
||||
setFocused(focus);
|
||||
onTriggerFocus?.(focus);
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
function keyupHandler(event: KeyboardEvent) {
|
||||
if (event.keyCode === 83 && event.target === document.body) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keyup', keyupHandler);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keyup', keyupHandler);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
id="search-box"
|
||||
className={classNames({
|
||||
'narrow-mode': responsive,
|
||||
focused,
|
||||
})}
|
||||
onClick={() => {
|
||||
inputRef.current.focus();
|
||||
}}
|
||||
>
|
||||
<SearchOutlined />
|
||||
<Input
|
||||
ref={inputRef}
|
||||
placeholder={searchPlaceholder}
|
||||
onFocus={() => {
|
||||
triggerFocus(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
triggerFocus(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
28
site/theme/template/Layout/Header/algolia-config.ts
Normal file
28
site/theme/template/Layout/Header/algolia-config.ts
Normal file
@ -0,0 +1,28 @@
|
||||
import { DocSearchHit } from '@docsearch/react/dist/esm/types';
|
||||
|
||||
let _internalATag: HTMLAnchorElement | null;
|
||||
|
||||
export function transformHitUrl(hitUrl: string) {
|
||||
_internalATag = _internalATag || document.createElement('a');
|
||||
// `new URL` is not supported in IE
|
||||
_internalATag.href = hitUrl;
|
||||
return `${_internalATag.pathname}${window.location.search || ''}${_internalATag.hash}`;
|
||||
}
|
||||
|
||||
export const AlgoliaConfig = {
|
||||
appId: 'BH4D9OD16A',
|
||||
apiKey: '60ac2c1a7d26ab713757e4a081e133d0',
|
||||
indexName: 'ant_design',
|
||||
getSearchParams(isZhCN: boolean) {
|
||||
return { facetFilters: [`tags:${isZhCN ? 'cn' : 'en'}`] };
|
||||
},
|
||||
transformData(hits: DocSearchHit[]) {
|
||||
hits.forEach(hit => {
|
||||
hit.url = transformHitUrl(hit.url);
|
||||
});
|
||||
return hits;
|
||||
},
|
||||
debug: false, // Set debug to true if you want to inspect the dropdown
|
||||
};
|
||||
|
||||
export type IAlgoliaConfig = typeof AlgoliaConfig;
|
@ -3,16 +3,19 @@ import { FormattedMessage, injectIntl } from 'react-intl';
|
||||
import classNames from 'classnames';
|
||||
import { UnorderedListOutlined } from '@ant-design/icons';
|
||||
import { Select, Row, Col, Popover, Button } from 'antd';
|
||||
import canUseDom from 'rc-util/lib/Dom/canUseDom';
|
||||
// import { browserHistory } from 'bisheng/router';
|
||||
|
||||
import * as utils from '../../utils';
|
||||
import packageJson from '../../../../../package.json';
|
||||
import Logo from './Logo';
|
||||
import SearchBox from './SearchBox';
|
||||
import { SearchBar } from './SearchBar';
|
||||
import More from './More';
|
||||
import Navigation from './Navigation';
|
||||
import Github from './Github';
|
||||
import SiteContext from '../SiteContext';
|
||||
import { ping } from '../../utils';
|
||||
import { AlgoliaConfig } from './algolia-config';
|
||||
|
||||
import './index.less';
|
||||
|
||||
@ -23,35 +26,6 @@ const { Option } = Select;
|
||||
|
||||
const antdVersion: string = packageJson.version;
|
||||
|
||||
let docsearch: any;
|
||||
if (typeof window !== 'undefined') {
|
||||
// eslint-disable-next-line global-require
|
||||
docsearch = require('docsearch.js');
|
||||
}
|
||||
|
||||
function initDocSearch(locale: string) {
|
||||
if (!docsearch) {
|
||||
return;
|
||||
}
|
||||
const lang = locale === 'zh-CN' ? 'cn' : 'en';
|
||||
docsearch({
|
||||
apiKey: '60ac2c1a7d26ab713757e4a081e133d0',
|
||||
indexName: 'ant_design',
|
||||
inputSelector: '#search-box input',
|
||||
algoliaOptions: { facetFilters: [`tags:${lang}`] },
|
||||
transformData(hits: { url: string }[]) {
|
||||
const a = document.createElement('a');
|
||||
hits.forEach(hit => {
|
||||
// `new URL` is not supported in IE
|
||||
a.href = hit.url;
|
||||
hit.url = `${a.pathname}${window.location.search || ''}${a.hash}`;
|
||||
});
|
||||
return hits;
|
||||
},
|
||||
debug: false, // Set debug to true if you want to inspect the dropdown
|
||||
});
|
||||
}
|
||||
|
||||
export interface HeaderProps {
|
||||
intl: {
|
||||
locale: string;
|
||||
@ -62,6 +36,44 @@ export interface HeaderProps {
|
||||
changeDirection: (direction: string) => void;
|
||||
}
|
||||
|
||||
let docsearch: any;
|
||||
const triggerDocSearchImport = () => {
|
||||
if (docsearch) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
return import('docsearch.js').then(ds => {
|
||||
docsearch = ds.default;
|
||||
});
|
||||
};
|
||||
|
||||
function initDocSearch(isZhCN: boolean) {
|
||||
if (!canUseDom()) {
|
||||
return;
|
||||
}
|
||||
|
||||
triggerDocSearchImport().then(() => {
|
||||
docsearch({
|
||||
appId: AlgoliaConfig.appId,
|
||||
apiKey: AlgoliaConfig.apiKey,
|
||||
indexName: AlgoliaConfig.indexName,
|
||||
inputSelector: '#search-box input',
|
||||
algoliaOptions: AlgoliaConfig.getSearchParams(isZhCN),
|
||||
transformData: AlgoliaConfig.transformData,
|
||||
debug: AlgoliaConfig.debug,
|
||||
// https://docsearch.algolia.com/docs/behavior#handleselected
|
||||
// handleSelected: (input, _$1, suggestion, _$2, context) => {
|
||||
// doesn't refresh
|
||||
// // Prevents the default behavior on click
|
||||
// if (context.selectionMethod === 'click') {
|
||||
// input.setVal('');
|
||||
// browserHistory.push(suggestion.url);
|
||||
// }
|
||||
// },
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
interface HeaderState {
|
||||
menuVisible: boolean;
|
||||
windowWidth: number;
|
||||
@ -84,7 +96,8 @@ class Header extends React.Component<HeaderProps, HeaderState> {
|
||||
componentDidMount() {
|
||||
const { intl, router } = this.props;
|
||||
router.listen(this.handleHideMenu);
|
||||
initDocSearch(intl.locale);
|
||||
|
||||
initDocSearch(intl.locale === 'zh');
|
||||
|
||||
window.addEventListener('resize', this.onWindowResize);
|
||||
this.onWindowResize();
|
||||
@ -237,15 +250,6 @@ class Header extends React.Component<HeaderProps, HeaderState> {
|
||||
isRTL,
|
||||
};
|
||||
|
||||
const searchBox = (
|
||||
<SearchBox
|
||||
key="search"
|
||||
{...sharedProps}
|
||||
responsive={responsive}
|
||||
onTriggerFocus={this.onTriggerSearching}
|
||||
/>
|
||||
);
|
||||
|
||||
const navigationNode = (
|
||||
<Navigation
|
||||
key="nav"
|
||||
@ -341,7 +345,13 @@ class Header extends React.Component<HeaderProps, HeaderState> {
|
||||
<Logo {...sharedProps} location={location} />
|
||||
</Col>
|
||||
<Col {...colProps[1]} className="menu-row">
|
||||
{searchBox}
|
||||
<SearchBar
|
||||
key="search"
|
||||
{...sharedProps}
|
||||
algoliaConfig={AlgoliaConfig}
|
||||
responsive={responsive}
|
||||
onTriggerFocus={this.onTriggerSearching}
|
||||
/>
|
||||
{!isMobile && menu}
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -6,6 +6,7 @@
|
||||
"antd/es/*": ["components/*"]
|
||||
},
|
||||
"strictNullChecks": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"esModuleInterop": true,
|
||||
"experimentalDecorators": true,
|
||||
|
9
typings/custom-typings.d.ts
vendored
9
typings/custom-typings.d.ts
vendored
@ -51,3 +51,12 @@ declare module '*.json' {
|
||||
export const version: string;
|
||||
export default value;
|
||||
}
|
||||
|
||||
declare module '@docsearch/react/style/modal';
|
||||
|
||||
declare module '@docsearch/react/modal' {
|
||||
import { DocSearchModal as value } from '@docsearch/react';
|
||||
export const DocSearchModal = value;
|
||||
}
|
||||
|
||||
declare module 'docsearch.js';
|
||||
|
Loading…
Reference in New Issue
Block a user