| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 | /* eslint-disable max-lines-per-function */import BaseComponent, { BaseProps } from '../_base/baseComponent';import React, { Children, ReactElement, ReactNode } from 'react';import PropTypes from 'prop-types';import cls from 'classnames';import { noop, get, isEqual } from 'lodash';import NavigationFoundation, { NavigationAdapter } from '@douyinfe/semi-foundation/navigation/foundation';import { strings, cssClasses, numbers } from '@douyinfe/semi-foundation/navigation/constants';import SubNav, { SubNavProps } from './SubNav';import Item, { NavItemProps } from './Item';import Footer, { NavFooterProps } from './Footer';import Header, { NavHeaderProps } from './Header';import NavContext from './nav-context';import LocaleConsumer from '../locale/localeConsumer';import '@douyinfe/semi-foundation/navigation/navigation.scss';export type { CollapseButtonProps } from './CollapseButton';export type { NavFooterProps } from './Footer';export type { NavHeaderProps } from './Header';export type { NavItemProps } from './Item';export type { ToggleIcon, SubNavProps } from './SubNav';export type Mode = 'vertical' | 'horizontal';export interface OnSelectedData {    itemKey: React.ReactText;    selectedKeys: React.ReactText[];    selectedItems: (NavItemProps | SubNavProps)[];    domEvent: React.MouseEvent;    isOpen: boolean}export interface SubNavPropsWithItems extends SubNavProps {    items?: (SubNavPropsWithItems | string)[]}export interface NavItemPropsWithItems extends NavItemProps {    items?: (NavItemPropsWithItems | string)[]}export type NavItems = (string | SubNavPropsWithItems | NavItemPropsWithItems)[];export interface NavProps extends BaseProps {    bodyStyle?: React.CSSProperties;    children?: React.ReactNode;    defaultIsCollapsed?: boolean;    defaultOpenKeys?: React.ReactText[];    defaultSelectedKeys?: React.ReactText[];    footer?: React.ReactNode | NavFooterProps;    header?: React.ReactNode | NavHeaderProps;    isCollapsed?: boolean;    items?: NavItems;    limitIndent?: boolean;    mode?: Mode;    multiple?: boolean;    openKeys?: React.ReactText[];    prefixCls?: string;    selectedKeys?: React.ReactText[];    subNavCloseDelay?: number;    subNavMotion?: boolean;    subNavOpenDelay?: number;    toggleIconPosition?: string;    tooltipHideDelay?: number;    tooltipShowDelay?: number;    getPopupContainer?: () => HTMLElement;    onClick?: (data: { itemKey?: React.ReactText; domEvent?: MouseEvent; isOpen?: boolean }) => void;    onCollapseChange?: (isCollapse: boolean) => void;    onDeselect?: (data?: any) => void;    onOpenChange?: (data: { itemKey?: (string | number); openKeys?: (string | number)[]; domEvent?: MouseEvent; isOpen?: boolean }) => void;    onSelect?: (data: OnSelectedData) => void;    renderWrapper?: ({ itemElement, isSubNav, isInSubNav, props }: { itemElement: ReactElement;isInSubNav: boolean; isSubNav: boolean; props: NavItemProps | SubNavProps }) => ReactNode}export interface NavState {    isCollapsed: boolean;    // calc state    openKeys: (string | number)[];    items: any[];    itemKeysMap: { [itemKey: string]: (string | number)[] };    selectedKeys: (string | number)[]}function createAddKeysFn(context: Nav, keyName: string | number) {    return function addKeys(...keys: (string | number)[]) {        const handleKeys = new Set(context.state[keyName]);        keys.forEach(key => key && handleKeys.add(key));        context.setState({ [keyName]: Array.from(handleKeys) } as any);    };}function createRemoveKeysFn(context: Nav, keyName: string) {    return function removeKeys(...keys: string[]) {        const handleKeys = new Set(context.state[keyName]);        keys.forEach(key => key && handleKeys.delete(key));        context.setState({ [keyName]: Array.from(handleKeys) } as any);    };}const { hasOwnProperty } = Object.prototype;class Nav extends BaseComponent<NavProps, NavState> {    static Sub = SubNav;    static Item = Item;    static Header = Header;    static Footer = Footer;    static propTypes = {        // Initial expanded SubNav navigation key array        defaultOpenKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),        openKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),        // Initial selected navigation key array        defaultSelectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),        selectedKeys: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.number])),        // Navigation type, now supports vertical, horizontal        mode: PropTypes.oneOf([...strings.MODE]),        // Triggered when selecting a navigation item        onSelect: PropTypes.func,        // Triggered when clicking a navigation item        onClick: PropTypes.func,        // SubNav expand/close callback        onOpenChange: PropTypes.func,        // Array of options (nested options can continue)        items: PropTypes.array,        // Is it in the state of being stowed to the sidebar        isCollapsed: PropTypes.bool,        defaultIsCollapsed: PropTypes.bool,        onCollapseChange: PropTypes.func,        multiple: PropTypes.bool,        onDeselect: PropTypes.func,        subNavMotion: PropTypes.oneOfType([PropTypes.bool, PropTypes.object, PropTypes.func]),        subNavCloseDelay: PropTypes.number,        subNavOpenDelay: PropTypes.number,        tooltipShowDelay: PropTypes.number,        tooltipHideDelay: PropTypes.number,        children: PropTypes.node,        style: PropTypes.object,        bodyStyle: PropTypes.object,        className: PropTypes.string,        toggleIconPosition: PropTypes.string,        prefixCls: PropTypes.string,        header: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),        footer: PropTypes.oneOfType([PropTypes.node, PropTypes.object]),        limitIndent: PropTypes.bool,        getPopupContainer: PropTypes.func,    };    static defaultProps = {        subNavCloseDelay: numbers.DEFAULT_SUBNAV_CLOSE_DELAY,        subNavOpenDelay: numbers.DEFAULT_SUBNAV_OPEN_DELAY,        tooltipHideDelay: numbers.DEFAULT_TOOLTIP_HIDE_DELAY,        tooltipShowDelay: numbers.DEFAULT_TOOLTIP_SHOW_DELAY,        onCollapseChange: noop,        onSelect: noop,        onClick: noop,        onOpenChange: noop,        toggleIconPosition: 'right',        limitIndent: true,        prefixCls: cssClasses.PREFIX,        subNavMotion: true,        // isOpen: false,        mode: strings.MODE_VERTICAL,        // defaultOpenKeys: [],        // defaultSelectedKeys: [],        // items: [],    };    itemsChanged: boolean;    constructor(props: NavProps) {        super(props);        this.foundation = new NavigationFoundation(this.adapter);        this.itemsChanged = true;        const { isCollapsed, defaultIsCollapsed, items, children } = props;        const initState = {            isCollapsed: Boolean(this.isControlled('isCollapsed') ? isCollapsed : defaultIsCollapsed),            // calc state            openKeys: [],            items: [],            itemKeysMap: {}, // itemKey to parentKeys            selectedKeys: [],        };        this.state = { ...initState };        if (items && items.length || children) {            const calcState = this.foundation.init('constructor');            this.state = {                ...initState,                ...calcState,            };        }    }    static getDerivedStateFromProps(props: NavProps, state: NavState) {        const willUpdateState: Partial<NavState> = {};        if (hasOwnProperty.call(props, 'isCollapsed') && props.isCollapsed !== state.isCollapsed) {            willUpdateState.isCollapsed = props.isCollapsed;        }        return willUpdateState;    }    componentDidMount() {        // override BaseComponent    }    componentDidUpdate(prevProps: NavProps, prevState: NavState) {        if (prevProps.items !== this.props.items || prevProps.children !== this.props.children) {            this.foundation.init();        } else {            this.foundation.handleItemsChange(false);            const { selectedKeys } = this.state;            if (this.props.selectedKeys && !isEqual(prevProps.selectedKeys, this.props.selectedKeys)) {                this.adapter.updateSelectedKeys(this.props.selectedKeys);            }            if (this.props.openKeys && !isEqual(prevProps.openKeys, this.props.openKeys)) {                this.adapter.updateOpenKeys(this.props.openKeys);            }            if (!isEqual(selectedKeys, prevState.selectedKeys)) {                const parentSelectKeys = this.foundation.selectLevelZeroParentKeys(null, ...selectedKeys);                this.adapter.addSelectedKeys(...parentSelectKeys);            }        }    }    get adapter(): NavigationAdapter<NavProps, NavState> {        return {            ...super.adapter,            notifySelect: (...args) => this.props.onSelect(...args),            notifyOpenChange: (...args) => this.props.onOpenChange(...args),            setIsCollapsed: isCollapsed => this.setState({ isCollapsed }),            notifyCollapseChange: (...args) => this.props.onCollapseChange(...args),            updateItems: items => this.setState({ items: [...items] }),            setItemKeysMap: itemKeysMap => this.setState({ itemKeysMap: { ...itemKeysMap } }),            addSelectedKeys: createAddKeysFn(this, 'selectedKeys'),            removeSelectedKeys: createRemoveKeysFn(this, 'selectedKeys'),            updateSelectedKeys: selectedKeys => this.setState({ selectedKeys: [...selectedKeys] }),            updateOpenKeys: openKeys => this.setState({ openKeys: [...openKeys] }),            addOpenKeys: createAddKeysFn(this, 'openKeys'),            removeOpenKeys: createRemoveKeysFn(this, 'openKeys'),            setItemsChanged: isChanged => {                this.itemsChanged = isChanged;            },        };    }    /**     * Render navigation items recursively     *     * @param {NavItem[]} items     * @returns {JSX.Element}     */    renderItems(items: (SubNavPropsWithItems | NavItemPropsWithItems)[] = [], level = 0) {        const finalDom = (            <>                {items.map((item, idx) => {                    if (Array.isArray(item.items) && item.items.length) {                        return (                            <SubNav key={item.itemKey || String(level) + idx} {...item as SubNavPropsWithItems} level={level}>                                {this.renderItems(item.items as (SubNavPropsWithItems | NavItemPropsWithItems)[], level + 1)}                            </SubNav>                        );                    } else {                        return <Item key={item.itemKey || String(level) + idx} {...item as NavItemPropsWithItems} level={level} />;                    }                })}            </>        );        return finalDom;    }    onCollapseChange = () => {        this.foundation.handleCollapseChange();    };    render() {        const {            children: originChildren,            mode,            onOpenChange,            onSelect,            onClick,            style,            className,            subNavCloseDelay,            subNavOpenDelay,            subNavMotion,            tooltipShowDelay,            tooltipHideDelay,            prefixCls,            bodyStyle,            footer,            header,            toggleIconPosition,            limitIndent,            renderWrapper,            getPopupContainer        } = this.props;        const { selectedKeys, openKeys, items, isCollapsed } = this.state;        const {            updateOpenKeys,            addOpenKeys,            removeOpenKeys,            updateSelectedKeys,            addSelectedKeys,            removeSelectedKeys,        } = this.adapter;        const finalStyle = { ...style };        let children: React.ReactNode[] = Children.toArray(originChildren);        const footers: React.ReactNode[] = [];        const headers: React.ReactNode[] = [];        if (React.isValidElement(footer)) {            footers.push(<Footer key={0}>{footer}</Footer>);        } else if (footer && typeof footer === 'object') {            footers.push(<Footer key={0} {...footer} />);        }        if (React.isValidElement(header)) {            headers.push(<Header key={0}>{header}</Header>);        } else if (header && typeof header === 'object') {            headers.push(<Header key={0} {...header} />);        }        if (Array.isArray(children) && children.length) {            children = [...children];            let childrenLength = children.length;            for (let i = 0; i < childrenLength; i++) {                const child = children[i];                if ((child as any).type === Footer || get(child, 'type.elementType') === 'NavFooter') {                    footers.push(child);                    children.splice(i, 1);                    i--;                    childrenLength--;                } else if ((child as any).type === Header || get(child, 'type.elementType') === 'NavHeader') {                    headers.push(child);                    children.splice(i, 1);                    i--;                    childrenLength--;                }            }        }        const finalCls = cls(prefixCls, className, {            [`${prefixCls}-collapsed`]: isCollapsed,            [`${prefixCls}-horizontal`]: mode === 'horizontal',            [`${prefixCls}-vertical`]: mode === 'vertical',        });        const headerListOuterCls = cls(`${prefixCls}-header-list-outer`, {            [`${prefixCls}-header-list-outer-collapsed`]: isCollapsed,        });        if (this.itemsChanged) {            this.adapter.setCache('itemElems', this.renderItems(items));        }        return (            <LocaleConsumer componentName="Navigation">                {locale => (                    <NavContext.Provider                        value={{                            subNavCloseDelay,                            subNavOpenDelay,                            subNavMotion,                            tooltipShowDelay,                            tooltipHideDelay,                            openKeys,                            openKeysIsControlled: this.isControlled('openKeys') && mode === 'vertical' && !isCollapsed,                            // canUpdateOpenKeys: mode === 'vertical' && !isCollapsed,                            canUpdateOpenKeys: true,                            selectedKeys,                            selectedKeysIsControlled: this.isControlled('selectedKeys'),                            isCollapsed,                            onCollapseChange: this.onCollapseChange,                            mode,                            onSelect,                            onOpenChange,                            updateOpenKeys,                            addOpenKeys,                            removeOpenKeys,                            updateSelectedKeys,                            addSelectedKeys,                            removeSelectedKeys,                            onClick,                            locale,                            prefixCls,                            toggleIconPosition,                            limitIndent,                            renderWrapper,                            getPopupContainer                        } as any}                    >                        <div className={finalCls} style={finalStyle}>                            <div className={`${prefixCls}-inner`}>                                <div className={headerListOuterCls}>                                    {headers}                                    <div style={bodyStyle} className={`${prefixCls}-list-wrapper`}>                                        <ul role="menu" aria-orientation={mode} className={`${prefixCls}-list`}>                                            {this.adapter.getCache('itemElems')}                                            {children}                                        </ul>                                    </div>                                </div>                                {footers}                            </div>                        </div>                    </NavContext.Provider>                )}            </LocaleConsumer>        );    }}export default Nav;
 |