mirror of
https://github.com/ant-design/ant-design.git
synced 2024-12-12 15:19:58 +08:00
e5451a95cb
* feat: add pointAtCenter prop for dropdown * feat: dropdown support arrow pointing at center * chore: code clean * chore: use getPlacements in tooltip * feat: dropdown support 'top' and 'bottom' * chore: update snapshot * chore: move placements to util folder * feat: make 'topCenter' and 'bottomCenter' in Dropdown deprecated * test: update snapshot * test: add test for topCenter and bottomCenter
541 lines
17 KiB
JavaScript
541 lines
17 KiB
JavaScript
import React, { Component } from 'react';
|
|
import { Link, browserHistory } from 'bisheng/router';
|
|
import { Row, Col, Menu, Affix, Tooltip, Avatar, Dropdown } from 'antd';
|
|
import { injectIntl } from 'react-intl';
|
|
import { LeftOutlined, RightOutlined, ExportOutlined } from '@ant-design/icons';
|
|
import ContributorsList from '@qixian.cs/github-contributors-list';
|
|
import classNames from 'classnames';
|
|
import get from 'lodash/get';
|
|
import MobileMenu from 'rc-drawer';
|
|
|
|
import ThemeIcon from './ThemeIcon';
|
|
import Article from './Article';
|
|
import PrevAndNext from './PrevAndNext';
|
|
import Footer from '../Layout/Footer';
|
|
import SiteContext from '../Layout/SiteContext';
|
|
import ComponentDoc from './ComponentDoc';
|
|
import ComponentOverview from './ComponentOverview';
|
|
import * as utils from '../utils';
|
|
|
|
const { SubMenu } = Menu;
|
|
|
|
function getModuleData(props) {
|
|
const { pathname } = props.location;
|
|
const moduleName = /^\/?components/.test(pathname)
|
|
? 'components'
|
|
: pathname
|
|
.split('/')
|
|
.filter(item => item)
|
|
.slice(0, 2)
|
|
.join('/');
|
|
const excludedSuffix = utils.isZhCN(props.location.pathname) ? 'en-US.md' : 'zh-CN.md';
|
|
let data;
|
|
switch (moduleName) {
|
|
case 'docs/react':
|
|
case 'changelog':
|
|
case 'changelog-cn':
|
|
data = [...props.picked['docs/react'], ...props.picked.changelog];
|
|
break;
|
|
default:
|
|
data = props.picked[moduleName];
|
|
}
|
|
return data.filter(({ meta }) => !meta.filename.endsWith(excludedSuffix));
|
|
}
|
|
|
|
function fileNameToPath(filename) {
|
|
const snippets = filename.replace(/(\/index)?((\.zh-cn)|(\.en-us))?\.md$/i, '').split('/');
|
|
return snippets[snippets.length - 1];
|
|
}
|
|
|
|
function getSideBarOpenKeys(nextProps) {
|
|
const { themeConfig } = nextProps;
|
|
const { pathname } = nextProps.location;
|
|
const locale = utils.isZhCN(pathname) ? 'zh-CN' : 'en-US';
|
|
const moduleData = getModuleData(nextProps);
|
|
const shouldOpenKeys = utils
|
|
.getMenuItems(moduleData, locale, themeConfig.categoryOrder, themeConfig.typeOrder)
|
|
.map(m => (m.title && m.title[locale]) || m.title);
|
|
return shouldOpenKeys;
|
|
}
|
|
|
|
function clearActiveToc() {
|
|
[].forEach.call(document.querySelectorAll('.toc-affix li a'), node => {
|
|
node.className = '';
|
|
});
|
|
}
|
|
|
|
function updateActiveToc(id) {
|
|
const currentNode = document.querySelectorAll(`.toc-affix li a[href="#${id}"]`)[0];
|
|
if (currentNode) {
|
|
clearActiveToc();
|
|
currentNode.className = 'current';
|
|
}
|
|
}
|
|
|
|
class MainContent extends Component {
|
|
static contextType = SiteContext;
|
|
|
|
state = {
|
|
openKeys: undefined,
|
|
};
|
|
|
|
componentDidMount() {
|
|
window.addEventListener('load', this.handleLoad);
|
|
window.addEventListener('hashchange', this.handleHashChange);
|
|
}
|
|
|
|
static getDerivedStateFromProps(props, state) {
|
|
if (!state.openKeys) {
|
|
return {
|
|
...state,
|
|
openKeys: getSideBarOpenKeys(props),
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
const { location } = this.props;
|
|
const { location: prevLocation = {} } = prevProps || {};
|
|
if (!prevProps || prevLocation.pathname !== location.pathname) {
|
|
this.bindScroller();
|
|
}
|
|
if (!window.location.hash && prevLocation.pathname !== location.pathname) {
|
|
clearActiveToc();
|
|
window.scrollTo(0, 0);
|
|
}
|
|
// when subMenu not equal
|
|
if (get(this.props, 'route.path') !== get(prevProps, 'route.path')) {
|
|
// reset menu OpenKeys
|
|
this.handleMenuOpenChange();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
if (this.scroller) {
|
|
this.scroller.destroy();
|
|
}
|
|
window.removeEventListener('load', this.handleLoad);
|
|
window.removeEventListener('hashchange', this.handleHashChange);
|
|
clearTimeout(this.timeout);
|
|
}
|
|
|
|
getMenuItems(footerNavIcons = {}) {
|
|
const {
|
|
themeConfig,
|
|
intl: { locale },
|
|
} = this.props;
|
|
const moduleData = getModuleData(this.props);
|
|
const menuItems = utils.getMenuItems(
|
|
moduleData,
|
|
locale,
|
|
themeConfig.categoryOrder,
|
|
themeConfig.typeOrder,
|
|
);
|
|
return menuItems.map(menuItem => {
|
|
if (menuItem.title === 'Overview' || menuItem.title === '组件总览') {
|
|
return menuItem.children.map(leaf => this.generateMenuItem(false, leaf, footerNavIcons));
|
|
}
|
|
if (menuItem.type === 'type') {
|
|
return (
|
|
<Menu.ItemGroup title={menuItem.title} key={menuItem.title}>
|
|
{menuItem.children
|
|
.sort((a, b) => a.title.localeCompare(b.title))
|
|
.map(leaf => this.generateMenuItem(false, leaf, footerNavIcons))}
|
|
</Menu.ItemGroup>
|
|
);
|
|
}
|
|
if (menuItem.children) {
|
|
return (
|
|
<SubMenu title={menuItem.title} key={menuItem.title}>
|
|
{menuItem.children.map(child => {
|
|
if (child.type === 'type') {
|
|
return (
|
|
<Menu.ItemGroup title={child.title} key={child.title}>
|
|
{child.children.map(leaf => this.generateMenuItem(false, leaf, footerNavIcons))}
|
|
</Menu.ItemGroup>
|
|
);
|
|
}
|
|
return this.generateMenuItem(false, child, footerNavIcons);
|
|
})}
|
|
</SubMenu>
|
|
);
|
|
}
|
|
return this.generateMenuItem(true, menuItem, footerNavIcons);
|
|
});
|
|
}
|
|
|
|
getFooterNav(menuItems, activeMenuItem) {
|
|
const menuItemsList = this.flattenMenu(menuItems);
|
|
let activeMenuItemIndex = -1;
|
|
menuItemsList.forEach((menuItem, i) => {
|
|
if (menuItem && menuItem.key === activeMenuItem) {
|
|
activeMenuItemIndex = i;
|
|
}
|
|
});
|
|
const prev = menuItemsList[activeMenuItemIndex - 1];
|
|
const next = menuItemsList[activeMenuItemIndex + 1];
|
|
return { prev, next };
|
|
}
|
|
|
|
getActiveMenuItem() {
|
|
const {
|
|
params: { children },
|
|
location,
|
|
} = this.props;
|
|
return (
|
|
(children && children.replace('-cn', '')) || location.pathname.replace(/(^\/|-cn$)/g, '')
|
|
);
|
|
}
|
|
|
|
handleMenuOpenChange = openKeys => {
|
|
this.setState({ openKeys });
|
|
};
|
|
|
|
handleLoad = () => {
|
|
if (window.location.hash) {
|
|
updateActiveToc(window.location.hash.replace(/^#/, ''));
|
|
}
|
|
this.bindScroller();
|
|
};
|
|
|
|
handleHashChange = () => {
|
|
this.timeout = setTimeout(() => {
|
|
updateActiveToc(window.location.hash.replace(/^#/, ''));
|
|
});
|
|
};
|
|
|
|
bindScroller() {
|
|
if (this.scroller) {
|
|
this.scroller.destroy();
|
|
}
|
|
if (!document.querySelector('.markdown > h2, .code-box')) {
|
|
return;
|
|
}
|
|
// eslint-disable-next-line global-require
|
|
require('intersection-observer');
|
|
// eslint-disable-next-line global-require
|
|
const scrollama = require('scrollama');
|
|
this.scroller = scrollama();
|
|
this.scroller
|
|
.setup({
|
|
step: '.markdown > h2, .code-box', // required
|
|
offset: '10px',
|
|
})
|
|
.onStepEnter(({ element }) => {
|
|
updateActiveToc(element.id);
|
|
});
|
|
}
|
|
|
|
generateMenuItem(isTop, item, { before = null, after = null }) {
|
|
const {
|
|
intl: { locale },
|
|
location,
|
|
} = this.props;
|
|
const key = fileNameToPath(item.filename);
|
|
if (!item.title) {
|
|
return null;
|
|
}
|
|
const title = item.title[locale] || item.title;
|
|
const text = isTop
|
|
? title
|
|
: [
|
|
<span key="english">{title}</span>,
|
|
<span className="chinese" key="chinese">
|
|
{item.subtitle}
|
|
</span>,
|
|
];
|
|
const { disabled } = item;
|
|
const url = item.filename.replace(/(\/index)?((\.zh-cn)|(\.en-us))?\.md$/i, '').toLowerCase();
|
|
|
|
const child = !item.link ? (
|
|
<Link
|
|
to={utils.getLocalizedPathname(
|
|
/^components/.test(url) ? `${url}/` : url,
|
|
locale === 'zh-CN',
|
|
location.query,
|
|
)}
|
|
disabled={disabled}
|
|
>
|
|
{before}
|
|
{text}
|
|
{after}
|
|
</Link>
|
|
) : (
|
|
<a
|
|
href={item.link}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
disabled={disabled}
|
|
className="menu-item-link-outside"
|
|
>
|
|
{before}
|
|
{text} <ExportOutlined />
|
|
{after}
|
|
</a>
|
|
);
|
|
|
|
return (
|
|
<Menu.Item key={key.toLowerCase()} disabled={disabled}>
|
|
{child}
|
|
</Menu.Item>
|
|
);
|
|
}
|
|
|
|
getThemeSwitchMenu() {
|
|
const { theme } = this.context;
|
|
const {
|
|
intl: { formatMessage },
|
|
} = this.props;
|
|
return (
|
|
<Menu onClick={({ key }) => this.changeThemeMode(key)} selectedKeys={[theme]}>
|
|
{[
|
|
{ type: 'default', text: formatMessage({ id: 'app.theme.switch.default' }) },
|
|
{ type: 'dark', text: formatMessage({ id: 'app.theme.switch.dark' }) },
|
|
{ type: 'compact', text: formatMessage({ id: 'app.theme.switch.compact' }) },
|
|
].map(({ type, text }) => (
|
|
<Menu.Item key={type}>{text}</Menu.Item>
|
|
))}
|
|
</Menu>
|
|
);
|
|
}
|
|
|
|
flattenMenu(menu) {
|
|
if (!menu) {
|
|
return null;
|
|
}
|
|
if (menu.type && menu.type.isMenuItem) {
|
|
return menu;
|
|
}
|
|
if (Array.isArray(menu)) {
|
|
return menu.reduce((acc, item) => acc.concat(this.flattenMenu(item)), []);
|
|
}
|
|
return this.flattenMenu((menu.props && menu.props.children) || menu.children);
|
|
}
|
|
|
|
changeThemeMode = theme => {
|
|
const { setTheme, theme: selectedTheme } = this.context;
|
|
const { pathname, hash, query } = this.props.location;
|
|
if (selectedTheme !== theme) {
|
|
setTheme(theme);
|
|
if (theme === 'default') {
|
|
document.documentElement.style.colorScheme = 'light';
|
|
setColor(false);
|
|
delete query.theme;
|
|
} else {
|
|
if (theme === 'dark') {
|
|
document.documentElement.style.colorScheme = 'dark';
|
|
setColor(true);
|
|
}
|
|
query.theme = theme;
|
|
}
|
|
browserHistory.push({
|
|
pathname: `/${pathname}`,
|
|
query,
|
|
hash,
|
|
});
|
|
}
|
|
};
|
|
|
|
renderContributors() {
|
|
const {
|
|
localizedPageData: { meta },
|
|
intl: { formatMessage },
|
|
} = this.props;
|
|
return (
|
|
<ContributorsList
|
|
className="contributors-list"
|
|
fileName={meta.filename}
|
|
renderItem={(item, loading) =>
|
|
loading ? (
|
|
<Avatar style={{ opacity: 0.3 }} />
|
|
) : (
|
|
<Tooltip
|
|
title={`${formatMessage({ id: 'app.content.contributors' })}: ${item.username}`}
|
|
key={item.username}
|
|
>
|
|
<a
|
|
href={`https://github.com/${item.username}`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
<Avatar src={item.url}>{item.username}</Avatar>
|
|
</a>
|
|
</Tooltip>
|
|
)
|
|
}
|
|
repo="ant-design"
|
|
owner="ant-design"
|
|
/>
|
|
);
|
|
}
|
|
|
|
renderMainContent({ theme, setIframeTheme }) {
|
|
const { localizedPageData, demos, location } = this.props;
|
|
if (location.pathname.includes('components/overview')) {
|
|
const type = utils.isZhCN(location.pathname) ? '重型组件' : 'ProComponents';
|
|
return (
|
|
<ComponentOverview
|
|
{...this.props}
|
|
doc={localizedPageData}
|
|
componentsData={getModuleData(this.props)
|
|
.filter(({ meta }) => meta.category === 'Components')
|
|
.concat([
|
|
{
|
|
meta: {
|
|
category: 'Components',
|
|
cover:
|
|
'https://gw.alipayobjects.com/zos/antfincdn/4n5H%24UX%24j/bianzu%2525204.svg',
|
|
filename: 'https://procomponents.ant.design/components/layout',
|
|
subtitle: '高级布局',
|
|
title: 'ProLayout',
|
|
type,
|
|
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
|
|
},
|
|
},
|
|
{
|
|
meta: {
|
|
category: 'Components',
|
|
cover: 'https://gw.alipayobjects.com/zos/antfincdn/mStei5BFC/bianzu%2525207.svg',
|
|
filename: 'https://procomponents.ant.design/components/form',
|
|
subtitle: '高级表单',
|
|
title: 'ProForm',
|
|
type,
|
|
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
|
|
},
|
|
},
|
|
{
|
|
meta: {
|
|
category: 'Components',
|
|
cover:
|
|
'https://gw.alipayobjects.com/zos/antfincdn/AwU0Cv%26Ju/bianzu%2525208.svg',
|
|
filename: 'https://procomponents.ant.design/components/table',
|
|
subtitle: '高级表格',
|
|
title: 'ProTable',
|
|
type,
|
|
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
|
|
},
|
|
},
|
|
{
|
|
meta: {
|
|
category: 'Components',
|
|
cover:
|
|
'https://gw.alipayobjects.com/zos/antfincdn/H0%26LSYYfh/bianzu%2525209.svg',
|
|
filename: 'https://procomponents.ant.design/components/descriptions',
|
|
subtitle: '高级定义列表',
|
|
title: 'ProDescriptions',
|
|
type,
|
|
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
|
|
},
|
|
},
|
|
{
|
|
meta: {
|
|
category: 'Components',
|
|
cover: 'https://gw.alipayobjects.com/zos/antfincdn/uZUmLtne5/bianzu%2525209.svg',
|
|
filename: 'https://procomponents.ant.design/components/list',
|
|
subtitle: '高级列表',
|
|
title: 'ProList',
|
|
type,
|
|
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
|
|
},
|
|
},
|
|
{
|
|
meta: {
|
|
category: 'Components',
|
|
cover: 'https://gw.alipayobjects.com/zos/antfincdn/N3eU432oA/bianzu%2525209.svg',
|
|
filename: 'https://procomponents.ant.design/components/list',
|
|
subtitle: '可编辑表格',
|
|
title: 'EditableProTable',
|
|
type,
|
|
tag: 'https://gw.alipayobjects.com/zos/antfincdn/OG4ajVYzh/bianzu%2525202.svg',
|
|
},
|
|
},
|
|
])}
|
|
/>
|
|
);
|
|
}
|
|
if (demos) {
|
|
return (
|
|
<>
|
|
<ComponentDoc
|
|
{...this.props}
|
|
doc={localizedPageData}
|
|
demos={demos}
|
|
theme={theme}
|
|
setIframeTheme={setIframeTheme}
|
|
/>
|
|
{this.renderContributors()}
|
|
</>
|
|
);
|
|
}
|
|
return (
|
|
<>
|
|
<Article {...this.props} content={localizedPageData} />
|
|
{this.renderContributors()}
|
|
</>
|
|
);
|
|
}
|
|
|
|
render() {
|
|
const { demos, location } = this.props;
|
|
const { openKeys } = this.state;
|
|
const { isMobile, theme, setIframeTheme } = this.context;
|
|
const activeMenuItem = this.getActiveMenuItem();
|
|
const menuItems = this.getMenuItems();
|
|
const menuItemsForFooterNav = this.getMenuItems({
|
|
before: <LeftOutlined className="footer-nav-icon-before" />,
|
|
after: <RightOutlined className="footer-nav-icon-after" />,
|
|
});
|
|
const { prev, next } = this.getFooterNav(menuItemsForFooterNav, activeMenuItem);
|
|
const mainContainerClass = classNames('main-container', {
|
|
'main-container-component': !!demos,
|
|
});
|
|
const menuChild = (
|
|
<Menu
|
|
inlineIndent={30}
|
|
className="aside-container menu-site"
|
|
mode="inline"
|
|
openKeys={openKeys}
|
|
selectedKeys={[activeMenuItem]}
|
|
onOpenChange={this.handleMenuOpenChange}
|
|
>
|
|
{menuItems}
|
|
</Menu>
|
|
);
|
|
const componentPage = /^\/?components/.test(location.pathname);
|
|
return (
|
|
<div className="main-wrapper">
|
|
<Row>
|
|
{isMobile ? (
|
|
<MobileMenu key="Mobile-menu" wrapperClassName="drawer-wrapper">
|
|
{menuChild}
|
|
</MobileMenu>
|
|
) : (
|
|
<Col xxl={4} xl={5} lg={6} md={6} sm={24} xs={24} className="main-menu">
|
|
<Affix>
|
|
<section className="main-menu-inner">{menuChild}</section>
|
|
</Affix>
|
|
</Col>
|
|
)}
|
|
<Col xxl={20} xl={19} lg={18} md={18} sm={24} xs={24}>
|
|
<section className={mainContainerClass}>
|
|
{this.renderMainContent({ theme, setIframeTheme })}
|
|
</section>
|
|
{componentPage && (
|
|
<div className="fixed-widgets">
|
|
<Dropdown overlay={this.getThemeSwitchMenu()} placement="top">
|
|
<Avatar className="fixed-widgets-avatar" size={44} icon={<ThemeIcon />} />
|
|
</Dropdown>
|
|
</div>
|
|
)}
|
|
<PrevAndNext prev={prev} next={next} />
|
|
<Footer location={location} />
|
|
</Col>
|
|
</Row>
|
|
</div>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default injectIntl(MainContent);
|