From a825ed4e66a0a2331ade42001e09356bc9798818 Mon Sep 17 00:00:00 2001 From: zombieJ Date: Mon, 4 Jun 2018 11:20:17 +0800 Subject: [PATCH] Directory Tree (#10139) Support DirTree. close #7749 --- components/checkbox/style/mixin.less | 6 +- components/style/themes/default.less | 8 + components/tree/DirectoryTree.tsx | 210 +++ components/tree/Tree.tsx | 156 ++ .../__tests__/__snapshots__/demo.test.js.snap | 261 +++- .../__snapshots__/directory.test.js.snap | 1265 +++++++++++++++++ components/tree/__tests__/directory.test.js | 104 ++ components/tree/__tests__/util.test.js | 35 + components/tree/demo/directory.md | 51 + components/tree/index.en-US.md | 7 + components/tree/index.tsx | 141 +- components/tree/index.zh-CN.md | 7 + components/tree/style/directory.less | 95 ++ components/tree/style/index.less | 64 +- components/tree/util.ts | 56 + package.json | 2 +- typings/custom-typings.d.ts | 1 + 17 files changed, 2268 insertions(+), 201 deletions(-) create mode 100644 components/tree/DirectoryTree.tsx create mode 100644 components/tree/Tree.tsx create mode 100644 components/tree/__tests__/__snapshots__/directory.test.js.snap create mode 100644 components/tree/__tests__/directory.test.js create mode 100644 components/tree/__tests__/util.test.js create mode 100644 components/tree/demo/directory.md create mode 100644 components/tree/style/directory.less create mode 100644 components/tree/util.ts diff --git a/components/checkbox/style/mixin.less b/components/checkbox/style/mixin.less index 21d2d57092..a66a059597 100644 --- a/components/checkbox/style/mixin.less +++ b/components/checkbox/style/mixin.less @@ -48,7 +48,7 @@ height: @checkbox-size; border: @border-width-base @border-style-base @border-color-base; border-radius: @border-radius-sm; - background-color: #fff; + background-color: @checkbox-check-color; transition: all .3s; &:after { @@ -61,7 +61,7 @@ display: table; width: @check-width; height: @check-height; - border: 2px solid #fff; + border: 2px solid @checkbox-check-color; border-top: 0; border-left: 0; content: ' '; @@ -105,7 +105,7 @@ transform: rotate(45deg) scale(1); position: absolute; display: table; - border: 2px solid #fff; + border: 2px solid @checkbox-check-color; border-top: 0; border-left: 0; content: ' '; diff --git a/components/style/themes/default.less b/components/style/themes/default.less index 692aa5c480..9f87415a31 100644 --- a/components/style/themes/default.less +++ b/components/style/themes/default.less @@ -157,6 +157,7 @@ // Checkbox @checkbox-size : 16px; @checkbox-color : @primary-color; +@checkbox-check-color : #fff; // Radio @radio-size : 16px; @@ -466,6 +467,13 @@ @slider-disabled-color: @disabled-color; @slider-disabled-background-color: @component-background; +// Tree +// --- +@tree-title-height: 24px; +@tree-child-padding: 18px; +@tree-directory-selected-color: #fff; +@tree-directory-selected-bg: @primary-color; + // Collapse // --- @collapse-header-padding: 12px 0 12px 40px; diff --git a/components/tree/DirectoryTree.tsx b/components/tree/DirectoryTree.tsx new file mode 100644 index 0000000000..63883094ab --- /dev/null +++ b/components/tree/DirectoryTree.tsx @@ -0,0 +1,210 @@ +import * as React from 'react'; +import classNames from 'classnames'; +import omit from 'omit.js'; +import debounce from 'lodash/debounce'; +import { getFullKeyList, calcExpandedKeys } from 'rc-tree/lib/util'; + +import Tree, { + TreeProps, AntdTreeNodeAttribute, + AntTreeNodeExpandedEvent, AntTreeNodeSelectedEvent, AntTreeNode, +} from './Tree'; +import { calcRangeKeys } from './util'; +import Icon from '../icon'; + +export type ExpandAction = false | 'click' | 'doubleClick'; + +export interface DirectoryTreeProps extends TreeProps { + expandAction?: ExpandAction; +} + +export interface DirectoryTreeState { + expandedKeys?: string[]; + selectedKeys?: string[]; +} + +function getIcon(props: AntdTreeNodeAttribute): React.ReactNode { + const { isLeaf, expanded } = props; + if (isLeaf) { + return ; + } + return ; +} + +export default class DirectoryTree extends React.Component { + static defaultProps = { + prefixCls: 'ant-tree', + showIcon: true, + expandAction: 'click', + }; + + state: DirectoryTreeState; + onDebounceExpand: (event: React.MouseEvent, node: AntTreeNode) => void; + + // Shift click usage + lastSelectedKey?: string; + cachedSelectedKeys?: string[]; + + constructor(props: DirectoryTreeProps) { + super(props); + + const { defaultExpandAll, defaultExpandParent, expandedKeys, defaultExpandedKeys } = props; + + // Selected keys + this.state = { + selectedKeys: props.selectedKeys || props.defaultSelectedKeys || [], + }; + + // Expanded keys + if (defaultExpandAll) { + this.state.expandedKeys = getFullKeyList(props.children); + } else if (defaultExpandParent) { + this.state.expandedKeys = calcExpandedKeys(expandedKeys || defaultExpandedKeys, props); + } else { + this.state.expandedKeys = defaultExpandedKeys; + } + + this.onDebounceExpand = debounce(this.expandFolderNode, 200, { + leading: true, + }); + } + + componentWillReceiveProps(nextProps: DirectoryTreeProps) { + if ('expandedKeys' in nextProps) { + this.setState({ expandedKeys: nextProps.expandedKeys }); + } + if ('selectedKeys' in nextProps) { + this.setState({ selectedKeys: nextProps.selectedKeys }); + } + } + + onExpand = (expandedKeys: string[], info: AntTreeNodeExpandedEvent) => { + const { onExpand } = this.props; + + this.setUncontrolledState({ expandedKeys }); + + // Call origin function + if (onExpand) { + return onExpand(expandedKeys, info); + } + + return undefined; + } + + onClick = (event: React.MouseEvent, node: AntTreeNode) => { + const { onClick, expandAction } = this.props; + + // Expand the tree + if (expandAction === 'click') { + this.onDebounceExpand(event, node); + } + + if (onClick) { + onClick(event, node); + } + } + + onDoubleClick = (event: React.MouseEvent, node: AntTreeNode) => { + const { onDoubleClick, expandAction } = this.props; + + // Expand the tree + if (expandAction === 'doubleClick') { + this.onDebounceExpand(event, node); + } + + if (onDoubleClick) { + onDoubleClick(event, node); + } + } + + onSelect = (keys: string[], event: AntTreeNodeSelectedEvent) => { + const { onSelect, multiple, children } = this.props; + const { expandedKeys = [], selectedKeys = [] } = this.state; + const { node, nativeEvent } = event; + const { eventKey = '' } = node.props; + + const newState: DirectoryTreeState = {}; + + // Windows / Mac single pick + const ctrlPick: boolean = nativeEvent.ctrlKey || nativeEvent.metaKey; + const shiftPick: boolean = nativeEvent.shiftKey; + + // Generate new selected keys + let newSelectedKeys = selectedKeys.slice(); + if (multiple && ctrlPick) { + // Control click + newSelectedKeys = keys; + this.lastSelectedKey = eventKey; + this.cachedSelectedKeys = newSelectedKeys; + } else if (multiple && shiftPick) { + // Shift click + newSelectedKeys = Array.from(new Set([ + ...this.cachedSelectedKeys || [], + ...calcRangeKeys(children, expandedKeys, eventKey, this.lastSelectedKey), + ])); + } else { + // Single click + newSelectedKeys = [eventKey]; + this.lastSelectedKey = eventKey; + this.cachedSelectedKeys = newSelectedKeys; + } + newState.selectedKeys = newSelectedKeys; + + if (onSelect) { + onSelect(newSelectedKeys, event); + } + + this.setUncontrolledState(newState); + } + + expandFolderNode = (event: React.MouseEvent, node: AntTreeNode) => { + const { expandedKeys = [] } = this.state; + const { eventKey = '', expanded, isLeaf } = node.props; + + if (isLeaf || event.shiftKey || event.metaKey || event.ctrlKey) { + return; + } + + let newExpandedKeys: string[] = expandedKeys.slice(); + const index = newExpandedKeys.indexOf(eventKey); + + if (expanded && index >= 0) { + newExpandedKeys.splice(index, 1); + } else if (!expanded && index === -1) { + newExpandedKeys.push(eventKey); + } + + this.setUncontrolledState({ + expandedKeys: newExpandedKeys, + }); + } + + setUncontrolledState = (state: DirectoryTreeState) => { + const newState = omit(state, Object.keys(this.props)); + if (Object.keys(newState).length) { + this.setState(newState); + } + } + + render() { + const { prefixCls, className, ...props } = this.props; + const { expandedKeys, selectedKeys } = this.state; + + const connectClassName = classNames(`${prefixCls}-directory`, className); + + return ( + + ); + } +} diff --git a/components/tree/Tree.tsx b/components/tree/Tree.tsx new file mode 100644 index 0000000000..d3fb636adb --- /dev/null +++ b/components/tree/Tree.tsx @@ -0,0 +1,156 @@ +import * as React from 'react'; +import RcTree, { TreeNode } from 'rc-tree'; +import DirectoryTree from './DirectoryTree'; +import classNames from 'classnames'; +import animation from '../_util/openAnimation'; + +export interface AntdTreeNodeAttribute { + eventKey: string; + prefixCls: string; + className: string; + expanded: boolean; + selected: boolean; + checked: boolean; + halfChecked: boolean; + children: React.ReactNode; + title: React.ReactNode; + pos: string; + dragOver: boolean; + dragOverGapTop: boolean; + dragOverGapBottom: boolean; + isLeaf: boolean; + selectable: boolean; + disabled: boolean; + disableCheckbox: boolean; +} +export interface AntTreeNodeProps { + disabled?: boolean; + disableCheckbox?: boolean; + title?: string | React.ReactNode; + key?: string; + eventKey?: string; + isLeaf?: boolean; + checked?: boolean; + expanded?: boolean; + selected?: boolean; + icon?: (nodeProps: AntdTreeNodeAttribute) => React.ReactNode | React.ReactNode; + children?: React.ReactNode; +} + +export interface AntTreeNode extends React.Component {} + +export interface AntTreeNodeBaseEvent { + node: AntTreeNode; + nativeEvent: MouseEvent; +} + +export interface AntTreeNodeCheckedEvent extends AntTreeNodeBaseEvent { + event: 'check'; + checked?: boolean; + checkedNodes?: AntTreeNode[]; +} + +export interface AntTreeNodeSelectedEvent extends AntTreeNodeBaseEvent { + event: 'select'; + selected?: boolean; + selectedNodes?: AntTreeNode[]; +} + +export interface AntTreeNodeExpandedEvent extends AntTreeNodeBaseEvent { + expanded?: boolean; +} + +export interface AntTreeNodeMouseEvent { + node: AntTreeNode; + event: React.MouseEventHandler; +} + +export interface TreeProps { + showLine?: boolean; + className?: string; + /** 是否支持多选 */ + multiple?: boolean; + /** 是否自动展开父节点 */ + autoExpandParent?: boolean; + /** checkable状态下节点选择完全受控(父子节点选中状态不再关联)*/ + checkStrictly?: boolean; + /** 是否支持选中 */ + checkable?: boolean; + /** 默认展开所有树节点 */ + defaultExpandAll?: boolean; + /** 默认展开对应树节点 */ + defaultExpandParent?: boolean; + /** 默认展开指定的树节点 */ + defaultExpandedKeys?: string[]; + /** (受控)展开指定的树节点 */ + expandedKeys?: string[]; + /** (受控)选中复选框的树节点 */ + checkedKeys?: string[] | { checked: string[]; halfChecked: string[] }; + /** 默认选中复选框的树节点 */ + defaultCheckedKeys?: string[]; + /** (受控)设置选中的树节点 */ + selectedKeys?: string[]; + /** 默认选中的树节点 */ + defaultSelectedKeys?: string[]; + /** 展开/收起节点时触发 */ + onExpand?: (expandedKeys: string[], info: AntTreeNodeExpandedEvent) => void | PromiseLike; + /** 点击复选框触发 */ + onCheck?: (checkedKeys: string[], e: AntTreeNodeCheckedEvent) => void; + /** 点击树节点触发 */ + onSelect?: (selectedKeys: string[], e: AntTreeNodeSelectedEvent) => void; + /** 单击树节点触发 */ + onClick?: (e: React.MouseEvent, node: AntTreeNode) => void; + /** 双击树节点触发 */ + onDoubleClick?: (e: React.MouseEvent, node: AntTreeNode) => void; + /** filter some AntTreeNodes as you need. it should return true */ + filterAntTreeNode?: (node: AntTreeNode) => boolean; + /** 异步加载数据 */ + loadData?: (node: AntTreeNode) => PromiseLike; + /** 响应右键点击 */ + onRightClick?: (options: AntTreeNodeMouseEvent) => void; + /** 设置节点可拖拽(IE>8)*/ + draggable?: boolean; + /** 开始拖拽时调用 */ + onDragStart?: (options: AntTreeNodeMouseEvent) => void; + /** dragenter 触发时调用 */ + onDragEnter?: (options: AntTreeNodeMouseEvent) => void; + /** dragover 触发时调用 */ + onDragOver?: (options: AntTreeNodeMouseEvent) => void; + /** dragleave 触发时调用 */ + onDragLeave?: (options: AntTreeNodeMouseEvent) => void; + /** drop 触发时调用 */ + onDrop?: (options: AntTreeNodeMouseEvent) => void; + style?: React.CSSProperties; + showIcon?: boolean; + icon?: (nodeProps: AntdTreeNodeAttribute) => React.ReactNode | React.ReactNode; + prefixCls?: string; + filterTreeNode?: (node: AntTreeNode) => boolean; + children?: React.ReactNode | React.ReactNode[]; +} + +export default class Tree extends React.Component { + static TreeNode = TreeNode; + static DirectoryTree = DirectoryTree; + + static defaultProps = { + prefixCls: 'ant-tree', + checkable: false, + showIcon: false, + openAnimation: animation, + }; + + render() { + const props = this.props; + const { prefixCls, className, showIcon } = props; + let checkable = props.checkable; + return ( + : checkable} + > + {this.props.children} + + ); + } +} diff --git a/components/tree/__tests__/__snapshots__/demo.test.js.snap b/components/tree/__tests__/__snapshots__/demo.test.js.snap index 3f489aada7..91ff69edd2 100644 --- a/components/tree/__tests__/__snapshots__/demo.test.js.snap +++ b/components/tree/__tests__/__snapshots__/demo.test.js.snap @@ -2,12 +2,12 @@ exports[`renders ./components/tree/demo/basic.md correctly 1`] = `
  • `; -exports[`renders ./components/tree/demo/draggable.md correctly 1`] = ` +exports[`renders ./components/tree/demo/directory.md correctly 1`] = `
    • + + + + + + + parent 0 + + +
        +
      • + + + + + + + leaf 0-0 + + +
      • +
      • + + + + + + + leaf 0-1 + + +
      • +
      +
    • +
    • + + + + + + + parent 1 + + +
        +
      • + + + + + + + leaf 1-0 + + +
      • +
      • + + + + + + + leaf 1-1 + + +
      • +
      +
    • +
    +`; + +exports[`renders ./components/tree/demo/draggable.md correctly 1`] = ` +
      +
      • +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      +`; + +exports[`Directory Tree defaultExpandParent 1`] = ` +
        +
      • + + + + + + + --- + + +
      • +
      • + + + + + + + --- + + +
      • +
      +`; + +exports[`Directory Tree expand click 1`] = ` +
        +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      • + + + + + + + --- + + +
      • +
      +`; + +exports[`Directory Tree expand click 2`] = ` +
        +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      • + + + + + + + --- + + +
      • +
      +`; + +exports[`Directory Tree expand double click 1`] = ` +
        +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      • + + + + + + + --- + + +
      • +
      +`; + +exports[`Directory Tree expand double click 2`] = ` +
        +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      • + + + + + + + --- + + +
      • +
      +`; + +exports[`Directory Tree expandedKeys update 1`] = ` +
        +
      • + + + + + + + --- + + +
      • +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      +`; + +exports[`Directory Tree group select 1`] = ` +
        +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      +`; + +exports[`Directory Tree group select 2`] = ` +
        +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      +`; + +exports[`Directory Tree selectedKeys update 1`] = ` +
        +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      • + + + + + + + --- + + +
          +
        • + + + + + + + --- + + +
        • +
        • + + + + + + + --- + + +
        • +
        +
      • +
      +`; diff --git a/components/tree/__tests__/directory.test.js b/components/tree/__tests__/directory.test.js new file mode 100644 index 0000000000..d3ad7a948a --- /dev/null +++ b/components/tree/__tests__/directory.test.js @@ -0,0 +1,104 @@ +import React from 'react'; +import { mount, render } from 'enzyme'; +import Tree from '../index'; + +const DirectoryTree = Tree.DirectoryTree; +const TreeNode = Tree.TreeNode; + +describe('Directory Tree', () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + function createTree(props) { + return ( + + + + + + + + + + + ); + } + + describe('expand', () => { + it('click', () => { + const wrapper = mount(createTree()); + + wrapper.find(TreeNode).find('.ant-tree-node-content-wrapper').at(0).simulate('click'); + expect(wrapper.render()).toMatchSnapshot(); + jest.runAllTimers(); + wrapper.find(TreeNode).find('.ant-tree-node-content-wrapper').at(0).simulate('click'); + expect(wrapper.render()).toMatchSnapshot(); + }); + + it('double click', () => { + const wrapper = mount(createTree({ expandAction: 'doubleClick' })); + + wrapper.find(TreeNode).find('.ant-tree-node-content-wrapper').at(0).simulate('doubleClick'); + expect(wrapper.render()).toMatchSnapshot(); + jest.runAllTimers(); + wrapper.find(TreeNode).find('.ant-tree-node-content-wrapper').at(0).simulate('doubleClick'); + expect(wrapper.render()).toMatchSnapshot(); + }); + }); + + it('defaultExpandAll', () => { + const wrapper = render(createTree({ defaultExpandAll: true })); + expect(wrapper).toMatchSnapshot(); + }); + + it('defaultExpandParent', () => { + const wrapper = render(createTree({ defaultExpandParent: true })); + expect(wrapper).toMatchSnapshot(); + }); + + it('expandedKeys update', () => { + const wrapper = mount(createTree()); + wrapper.setProps({ expandedKeys: ['0-1'] }); + expect(wrapper.render()).toMatchSnapshot(); + }); + + it('selectedKeys update', () => { + const wrapper = mount(createTree({ defaultExpandAll: true })); + wrapper.setProps({ selectedKeys: ['0-1-0'] }); + expect(wrapper.render()).toMatchSnapshot(); + }); + + it('group select', () => { + let nativeEventProto = null; + const wrapper = mount(createTree({ + defaultExpandAll: true, + expandAction: 'doubleClick', + multiple: true, + onClick: (e) => { + nativeEventProto = Object.getPrototypeOf(e.nativeEvent); + }, + })); + + wrapper.find(TreeNode).find('.ant-tree-node-content-wrapper').at(0).simulate('click'); + + // React not simulate full of NativeEvent. Hook it. + // Ref: https://github.com/facebook/react/blob/master/packages/react-dom/src/test-utils/ReactTestUtils.js#L360 + nativeEventProto.ctrlKey = true; + + wrapper.find(TreeNode).find('.ant-tree-node-content-wrapper').at(1).simulate('click'); + expect(wrapper.render()).toMatchSnapshot(); + + delete nativeEventProto.ctrlKey; + nativeEventProto.shiftKey = true; + + wrapper.find(TreeNode).find('.ant-tree-node-content-wrapper').at(4).simulate('click'); + expect(wrapper.render()).toMatchSnapshot(); + + delete nativeEventProto.shiftKey; + }); +}); diff --git a/components/tree/__tests__/util.test.js b/components/tree/__tests__/util.test.js new file mode 100644 index 0000000000..f5a331674a --- /dev/null +++ b/components/tree/__tests__/util.test.js @@ -0,0 +1,35 @@ +import React from 'react'; +import { mount } from 'enzyme'; +import Tree from '../index'; +import { calcRangeKeys } from '../util'; + +const TreeNode = Tree.TreeNode; + +describe('Tree util', () => { + it('calc range keys', () => { + const wrapper = mount( + + + + + + + + + + + + + + + + + + ); + + const { children } = wrapper.find(Tree).props(); + const keys = calcRangeKeys(children, ['0-0', '0-2', '0-2-0'], '0-2-0-1', '0-0-0'); + const target = ['0-0-0', '0-0-1', '0-1', '0-2', '0-2-0', '0-2-0-0', '0-2-0-1']; + expect(keys.sort()).toEqual(target.sort()); + }); +}); diff --git a/components/tree/demo/directory.md b/components/tree/demo/directory.md new file mode 100644 index 0000000000..33852edba2 --- /dev/null +++ b/components/tree/demo/directory.md @@ -0,0 +1,51 @@ +--- +order: 7 +title: + zh-CN: 目录 + en-US: directory +--- + +## zh-CN + +内置的目录树,`multiple` 模式支持 `ctrl(Windows)` / `command(Mac)` 复选。 + +## en-US + +Built-in directory tree. `multiple` support `ctrl(Windows)` / `command(Mac)` selection. + +````jsx +import { Tree } from 'antd'; +const DirectoryTree = Tree.DirectoryTree; +const TreeNode = Tree.TreeNode; + +class Demo extends React.Component { + onSelect = () => { + console.log('Trigger Select'); + }; + onExpand = () => { + console.log('Trigger Expand'); + }; + + render() { + return ( + + + + + + + + + + + ); + } +} + +ReactDOM.render(, mountNode); +```` diff --git a/components/tree/index.en-US.md b/components/tree/index.en-US.md index 059d0df026..899696b95e 100644 --- a/components/tree/index.en-US.md +++ b/components/tree/index.en-US.md @@ -55,6 +55,13 @@ Almost anything can be represented in a tree structure. Examples include directo | selectable | Set whether the treeNode can be selected | boolean | true | | title | Title | string\|ReactNode | '---' | +### DirectoryTree props + +| Property | Description | Type | Default | +| --- | --- | --- | --- | +| expandAction | Directory open logic, optional `false` `'click'` `'doubleClick'` | string | click | + + ## Note Before `3.4.0`: diff --git a/components/tree/index.tsx b/components/tree/index.tsx index 1b5ad5dde4..a359be9315 100644 --- a/components/tree/index.tsx +++ b/components/tree/index.tsx @@ -1,132 +1,15 @@ -import * as React from 'react'; -import RcTree, { TreeNode } from 'rc-tree'; -import animation from '../_util/openAnimation'; +import Tree from './Tree'; -export interface AntdTreeNodeAttribute { - eventKey: string; - prefixCls: string; - className: string; - expanded: boolean; - selected: boolean; - checked: boolean; - halfChecked: boolean; - children: React.ReactNode; - title: React.ReactNode; - pos: string; - dragOver: boolean; - dragOverGapTop: boolean; - dragOverGapBottom: boolean; - isLeaf: boolean; - selectable: boolean; - disabled: boolean; - disableCheckbox: boolean; -} -export interface AntTreeNodeProps { - disabled?: boolean; - disableCheckbox?: boolean; - title?: string | React.ReactNode; - key?: string; - isLeaf?: boolean; - icon?: (treeNode: AntdTreeNodeAttribute) => React.ReactNode | React.ReactNode; - children?: React.ReactNode; -} +export { + TreeProps, + AntTreeNode, + AntTreeNodeMouseEvent, AntTreeNodeExpandedEvent, AntTreeNodeCheckedEvent, AntTreeNodeSelectedEvent, + AntdTreeNodeAttribute, AntTreeNodeProps, +} from './Tree'; -export interface AntTreeNode extends React.Component {} +export { + ExpandAction as DirectoryTreeExpandAction, + DirectoryTreeProps, +} from './DirectoryTree'; -export interface AntTreeNodeEvent { - event: 'check' | 'select'; - node: AntTreeNode; - checked?: boolean; - checkedNodes?: AntTreeNode[]; - selected?: boolean; - selectedNodes?: AntTreeNode[]; -} - -export interface AntTreeNodeMouseEvent { - node: AntTreeNode; - event: React.MouseEventHandler; -} - -export interface TreeProps { - showLine?: boolean; - className?: string; - /** 是否支持多选 */ - multiple?: boolean; - /** 是否自动展开父节点 */ - autoExpandParent?: boolean; - /** checkable状态下节点选择完全受控(父子节点选中状态不再关联)*/ - checkStrictly?: boolean; - /** 是否支持选中 */ - checkable?: boolean; - /** 默认展开所有树节点 */ - defaultExpandAll?: boolean; - /** 默认展开指定的树节点 */ - defaultExpandedKeys?: string[]; - /** (受控)展开指定的树节点 */ - expandedKeys?: string[]; - /** (受控)选中复选框的树节点 */ - checkedKeys?: string[] | { checked: string[]; halfChecked: string[] }; - /** 默认选中复选框的树节点 */ - defaultCheckedKeys?: string[]; - /** (受控)设置选中的树节点 */ - selectedKeys?: string[]; - /** 默认选中的树节点 */ - defaultSelectedKeys?: string[]; - /** 展开/收起节点时触发 */ - onExpand?: ( - expandedKeys: string[], - info: { node: AntTreeNode; expanded: boolean; }, - ) => void | PromiseLike; - /** 点击复选框触发 */ - onCheck?: (checkedKeys: string[], e: AntTreeNodeEvent) => void; - /** 点击树节点触发 */ - onSelect?: (selectedKeys: string[], e: AntTreeNodeEvent) => void; - /** filter some AntTreeNodes as you need. it should return true */ - filterAntTreeNode?: (node: AntTreeNode) => boolean; - /** 异步加载数据 */ - loadData?: (node: AntTreeNode) => PromiseLike; - /** 响应右键点击 */ - onRightClick?: (options: AntTreeNodeMouseEvent) => void; - /** 设置节点可拖拽(IE>8)*/ - draggable?: boolean; - /** 开始拖拽时调用 */ - onDragStart?: (options: AntTreeNodeMouseEvent) => void; - /** dragenter 触发时调用 */ - onDragEnter?: (options: AntTreeNodeMouseEvent) => void; - /** dragover 触发时调用 */ - onDragOver?: (options: AntTreeNodeMouseEvent) => void; - /** dragleave 触发时调用 */ - onDragLeave?: (options: AntTreeNodeMouseEvent) => void; - /** drop 触发时调用 */ - onDrop?: (options: AntTreeNodeMouseEvent) => void; - style?: React.CSSProperties; - showIcon?: boolean; - prefixCls?: string; - filterTreeNode?: (node: AntTreeNode) => boolean; -} - -export default class Tree extends React.Component { - static TreeNode = TreeNode; - - static defaultProps = { - prefixCls: 'ant-tree', - checkable: false, - showIcon: false, - openAnimation: animation, - }; - - render() { - const props = this.props; - const { prefixCls, className } = props; - let checkable = props.checkable; - return ( - : checkable} - > - {this.props.children} - - ); - } -} +export default Tree; diff --git a/components/tree/index.zh-CN.md b/components/tree/index.zh-CN.md index ded13b3896..be7095661b 100644 --- a/components/tree/index.zh-CN.md +++ b/components/tree/index.zh-CN.md @@ -56,6 +56,13 @@ subtitle: 树形控件 | selectable | 设置节点是否可被选中 | boolean | true | | title | 标题 | string\|ReactNode | '---' | +### DirectoryTree props + +| 参数 | 说明 | 类型 | 默认值 | +| --- | --- | --- | --- | +| expandAction | 目录展开逻辑,可选 `false` `'click'` `'doubleClick'` | string | click | + + ## 注意 在 `3.4.0` 之前: diff --git a/components/tree/style/directory.less b/components/tree/style/directory.less new file mode 100644 index 0000000000..18e8ab6608 --- /dev/null +++ b/components/tree/style/directory.less @@ -0,0 +1,95 @@ +@import "../../style/themes/default"; + +@tree-prefix-cls: ~"@{ant-prefix}-tree"; + +.@{tree-prefix-cls} { + &.@{tree-prefix-cls}-directory { + position: relative; + + // Stretch selector width + > li, + .@{tree-prefix-cls}-child-tree > li { + span { + &.@{tree-prefix-cls}-switcher { + position: relative; + z-index: 1; + + &.@{tree-prefix-cls}-switcher-noop { + pointer-events: none; + } + } + + &.@{tree-prefix-cls}-checkbox { + position: relative; + z-index: 1; + } + + &.@{tree-prefix-cls}-node-content-wrapper { + user-select: none; + border-radius: 0; + + &:hover { + background: transparent; + + &:before { + background: @item-hover-bg; + } + } + + &.@{tree-prefix-cls}-node-selected { + color: @tree-directory-selected-color; + background: transparent; + } + + &:before { + content: ''; + position: absolute; + left: 0; + right: 0; + height: @tree-title-height; + transition: all .3s; + } + + > span { + position: relative; + z-index: 1; + } + } + } + + &.@{tree-prefix-cls}-treenode-selected { + > span { + &.@{tree-prefix-cls}-switcher { + color: @tree-directory-selected-color; + } + + &.@{tree-prefix-cls}-checkbox { + .@{tree-prefix-cls}-checkbox-inner { + border-color: @primary-color; + } + + &.@{tree-prefix-cls}-checkbox-checked { + &:after { + border-color: @checkbox-check-color; + } + + .@{tree-prefix-cls}-checkbox-inner { + background: @checkbox-check-color; + + &:after { + border-color: @primary-color; + } + } + } + } + + &.@{tree-prefix-cls}-node-content-wrapper { + &:before { + background: @tree-directory-selected-bg; + } + } + } + } + } + } +} diff --git a/components/tree/style/index.less b/components/tree/style/index.less index 6f086b895b..dfe9ece962 100644 --- a/components/tree/style/index.less +++ b/components/tree/style/index.less @@ -2,6 +2,7 @@ @import "../../style/mixins/index"; @import "../../checkbox/style/mixin"; @import "./mixin"; +@import "./directory"; @tree-prefix-cls: ~"@{ant-prefix}-tree"; @tree-showline-icon-color: @text-color-secondary; @@ -58,9 +59,37 @@ font-weight: 500 !important; } } + + // When node is loading + &.@{tree-prefix-cls}-treenode-loading { + span { + &.@{tree-prefix-cls}-switcher { + &.@{tree-prefix-cls}-switcher_open, + &.@{tree-prefix-cls}-switcher_close { + &:before { + display: inline-block; + position: absolute; + left: 0; + width: 24px; + height: 24px; + .iconfont-font("\E64D"); + animation: loadingCircle 1s infinite linear; + color: @primary-color; + transform: none; + font-size: 14px; + } + + :root &:after { + opacity: 0; + } + } + } + } + } + ul { margin: 0; - padding: 0 0 0 18px; + padding: 0 0 0 @tree-child-padding; } .@{tree-prefix-cls}-node-content-wrapper { display: inline-block; @@ -72,9 +101,8 @@ vertical-align: top; color: @text-color; transition: all .3s; - position: relative; - height: 24px; - line-height: 24px; + height: @tree-title-height; + line-height: @tree-title-height; &:hover { background-color: @item-hover-bg; } @@ -91,7 +119,7 @@ margin: 0; width: 24px; height: 24px; - line-height: 24px; + line-height: @tree-title-height; display: inline-block; vertical-align: top; border: 0 none; @@ -99,21 +127,10 @@ outline: none; text-align: center; } - &.@{tree-prefix-cls}-icon_loading { - position: absolute; - left: 0; - top: 1px; - background: #fff; - transform: translateX(-100%); - transition: all .3s; - &:after { - display: inline-block; - .iconfont-font("\E64D"); - animation: loadingCircle 1s infinite linear; - color: @primary-color; - } - } + &.@{tree-prefix-cls}-switcher { + position: relative; + &.@{tree-prefix-cls}-switcher-noop { cursor: default; } @@ -137,6 +154,7 @@ } } } + > li { &:first-child { padding-top: 7px; @@ -200,4 +218,12 @@ margin: 22px 0; } } + + &.@{tree-prefix-cls}-icon-hide { + .@{tree-prefix-cls}-treenode-loading { + .@{tree-prefix-cls}-iconEle { + display: none; + } + } + } } diff --git a/components/tree/util.ts b/components/tree/util.ts new file mode 100644 index 0000000000..1c991886b2 --- /dev/null +++ b/components/tree/util.ts @@ -0,0 +1,56 @@ +import * as React from 'react'; +import { traverseTreeNodes } from 'rc-tree/lib/util'; + +export interface TraverseData { + key: string, +} + +enum Record { + None, + Start, + End, +} + +/** 计算选中范围,只考虑expanded情况以优化性能 */ +export function calcRangeKeys(nodeList: React.ReactNode | React.ReactNode[], expandedKeys: string[], startKey?: string, endKey?: string): string[] { + const keys: string[] = []; + let record: Record = Record.None; + + if (startKey && startKey === endKey) { + return [startKey]; + } + if (!startKey || !endKey) { + return []; + } + + function matchKey(key: string) { + return key === startKey || key === endKey; + } + + traverseTreeNodes(nodeList, ({ key }: TraverseData) => { + if (record === Record.End) { + return false; + } + + if (matchKey(key)) { + // Match test + keys.push(key); + + if (record === Record.None) { + record = Record.Start; + } else if (record === Record.Start) { + record = Record.End; + return false; + } + } else if (record === Record.Start) { + // Append selection + keys.push(key); + } + + if (expandedKeys.indexOf(key) === -1) { + return false; + } + }); + + return keys; +} diff --git a/package.json b/package.json index 39c84373f4..a1d6469cb5 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "rc-tabs": "~9.2.0", "rc-time-picker": "~3.3.0", "rc-tooltip": "~3.7.0", - "rc-tree": "~1.8.0", + "rc-tree": "~1.11.0", "rc-tree-select": "~1.12.0", "rc-upload": "~2.4.0", "rc-util": "^4.0.4", diff --git a/typings/custom-typings.d.ts b/typings/custom-typings.d.ts index adc5c663ce..91437563c0 100644 --- a/typings/custom-typings.d.ts +++ b/typings/custom-typings.d.ts @@ -43,6 +43,7 @@ declare module 'rc-menu'; declare module 'rc-tabs*'; declare module 'rc-tree'; +declare module 'rc-tree/lib/util'; declare module 'rc-tooltip*';