Directory Tree (#10139)

Support DirTree. close #7749
This commit is contained in:
zombieJ 2018-06-04 11:20:17 +08:00 committed by GitHub
parent ff24f1a978
commit a825ed4e66
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 2268 additions and 201 deletions

View File

@ -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: ' ';

View File

@ -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;

View File

@ -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 <Icon type="file" />;
}
return <Icon type={expanded ? 'folder-open' : 'folder'} />;
}
export default class DirectoryTree extends React.Component<DirectoryTreeProps, DirectoryTreeState> {
static defaultProps = {
prefixCls: 'ant-tree',
showIcon: true,
expandAction: 'click',
};
state: DirectoryTreeState;
onDebounceExpand: (event: React.MouseEvent<HTMLElement>, 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<HTMLElement>, 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<HTMLElement>, 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<HTMLElement>, 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 (
<Tree
icon={getIcon}
{...props}
prefixCls={prefixCls}
className={connectClassName}
expandedKeys={expandedKeys}
selectedKeys={selectedKeys}
onSelect={this.onSelect}
onClick={this.onClick}
onDoubleClick={this.onDoubleClick}
onExpand={this.onExpand}
/>
);
}
}

156
components/tree/Tree.tsx Normal file
View File

@ -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<AntTreeNodeProps, {}> {}
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<any>;
}
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<any>;
/** 点击复选框触发 */
onCheck?: (checkedKeys: string[], e: AntTreeNodeCheckedEvent) => void;
/** 点击树节点触发 */
onSelect?: (selectedKeys: string[], e: AntTreeNodeSelectedEvent) => void;
/** 单击树节点触发 */
onClick?: (e: React.MouseEvent<HTMLElement>, node: AntTreeNode) => void;
/** 双击树节点触发 */
onDoubleClick?: (e: React.MouseEvent<HTMLElement>, node: AntTreeNode) => void;
/** filter some AntTreeNodes as you need. it should return true */
filterAntTreeNode?: (node: AntTreeNode) => boolean;
/** 异步加载数据 */
loadData?: (node: AntTreeNode) => PromiseLike<any>;
/** 响应右键点击 */
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<TreeProps, any> {
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 (
<RcTree
{...props}
className={classNames(!showIcon && `${prefixCls}-icon-hide`, className)}
checkable={checkable ? <span className={`${prefixCls}-checkbox-inner`} /> : checkable}
>
{this.props.children}
</RcTree>
);
}
}

View File

@ -2,12 +2,12 @@
exports[`renders ./components/tree/demo/basic.md correctly 1`] = `
<ul
class="ant-tree"
class="ant-tree ant-tree-icon-hide"
role="tree-node"
unselectable="on"
>
<li
class=""
class="ant-tree-treenode-switcher-open ant-tree-treenode-checkbox-checked"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -34,7 +34,7 @@ exports[`renders ./components/tree/demo/basic.md correctly 1`] = `
data-expanded="true"
>
<li
class="ant-tree-treenode-disabled"
class="ant-tree-treenode-disabled ant-tree-treenode-switcher-open ant-tree-treenode-checkbox-checked ant-tree-treenode-selected"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -61,7 +61,7 @@ exports[`renders ./components/tree/demo/basic.md correctly 1`] = `
data-expanded="true"
>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -85,7 +85,7 @@ exports[`renders ./components/tree/demo/basic.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -111,7 +111,7 @@ exports[`renders ./components/tree/demo/basic.md correctly 1`] = `
</ul>
</li>
<li
class=""
class="ant-tree-treenode-switcher-open ant-tree-treenode-checkbox-checked"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -138,7 +138,7 @@ exports[`renders ./components/tree/demo/basic.md correctly 1`] = `
data-expanded="true"
>
<li
class=""
class="ant-tree-treenode-switcher-close ant-tree-treenode-checkbox-checked"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -174,12 +174,12 @@ exports[`renders ./components/tree/demo/basic.md correctly 1`] = `
exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
<ul
class="ant-tree"
class="ant-tree ant-tree-icon-hide"
role="tree-node"
unselectable="on"
>
<li
class=""
class="ant-tree-treenode-switcher-open ant-tree-treenode-checkbox-indeterminate"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -206,7 +206,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
data-expanded="true"
>
<li
class=""
class="ant-tree-treenode-switcher-open ant-tree-treenode-checkbox-checked"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -233,7 +233,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
data-expanded="true"
>
<li
class=""
class="ant-tree-treenode-switcher-close ant-tree-treenode-checkbox-checked"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -257,7 +257,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close ant-tree-treenode-checkbox-checked"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -281,7 +281,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close ant-tree-treenode-checkbox-checked"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -307,7 +307,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
</ul>
</li>
<li
class=""
class="ant-tree-treenode-switcher-open"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -334,7 +334,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
data-expanded="true"
>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -358,7 +358,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -382,7 +382,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -408,7 +408,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
</ul>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -434,7 +434,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
</ul>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher_close"
@ -458,7 +458,7 @@ exports[`renders ./components/tree/demo/basic-controlled.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -491,7 +491,7 @@ exports[`renders ./components/tree/demo/customized-icon.md correctly 1`] = `
unselectable="on"
>
<li
class=""
class="ant-tree-treenode-switcher-open"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -518,7 +518,7 @@ exports[`renders ./components/tree/demo/customized-icon.md correctly 1`] = `
data-expanded="true"
>
<li
class=""
class="ant-tree-treenode-switcher-open ant-tree-treenode-selected"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -542,7 +542,7 @@ exports[`renders ./components/tree/demo/customized-icon.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-open"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -570,14 +570,177 @@ exports[`renders ./components/tree/demo/customized-icon.md correctly 1`] = `
</ul>
`;
exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
exports[`renders ./components/tree/demo/directory.md correctly 1`] = `
<ul
class="ant-tree draggable-tree"
class="ant-tree ant-tree-directory"
role="tree-node"
unselectable="on"
>
<li
class=""
class="ant-tree-treenode-switcher-open"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
/>
<span
class="ant-tree-node-content-wrapper ant-tree-node-content-wrapper-open"
title="parent 0"
>
<span
class="ant-tree-iconEle ant-tree-icon__customize"
>
<i
class="anticon anticon-folder-open"
/>
</span>
<span
class="ant-tree-title"
>
parent 0
</span>
</span>
<ul
class="ant-tree-child-tree ant-tree-child-tree-open"
data-expanded="true"
>
<li
class=""
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
/>
<span
class="ant-tree-node-content-wrapper ant-tree-node-content-wrapper-normal"
title="leaf 0-0"
>
<span
class="ant-tree-iconEle ant-tree-icon__customize"
>
<i
class="anticon anticon-file"
/>
</span>
<span
class="ant-tree-title"
>
leaf 0-0
</span>
</span>
</li>
<li
class=""
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
/>
<span
class="ant-tree-node-content-wrapper ant-tree-node-content-wrapper-normal"
title="leaf 0-1"
>
<span
class="ant-tree-iconEle ant-tree-icon__customize"
>
<i
class="anticon anticon-file"
/>
</span>
<span
class="ant-tree-title"
>
leaf 0-1
</span>
</span>
</li>
</ul>
</li>
<li
class="ant-tree-treenode-switcher-open"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
/>
<span
class="ant-tree-node-content-wrapper ant-tree-node-content-wrapper-open"
title="parent 1"
>
<span
class="ant-tree-iconEle ant-tree-icon__customize"
>
<i
class="anticon anticon-folder-open"
/>
</span>
<span
class="ant-tree-title"
>
parent 1
</span>
</span>
<ul
class="ant-tree-child-tree ant-tree-child-tree-open"
data-expanded="true"
>
<li
class=""
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
/>
<span
class="ant-tree-node-content-wrapper ant-tree-node-content-wrapper-normal"
title="leaf 1-0"
>
<span
class="ant-tree-iconEle ant-tree-icon__customize"
>
<i
class="anticon anticon-file"
/>
</span>
<span
class="ant-tree-title"
>
leaf 1-0
</span>
</span>
</li>
<li
class=""
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
/>
<span
class="ant-tree-node-content-wrapper ant-tree-node-content-wrapper-normal"
title="leaf 1-1"
>
<span
class="ant-tree-iconEle ant-tree-icon__customize"
>
<i
class="anticon anticon-file"
/>
</span>
<span
class="ant-tree-title"
>
leaf 1-1
</span>
</span>
</li>
</ul>
</li>
</ul>
`;
exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
<ul
class="ant-tree ant-tree-icon-hide draggable-tree"
role="tree-node"
unselectable="on"
>
<li
class="ant-tree-treenode-switcher-open"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -599,7 +762,7 @@ exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
data-expanded="true"
>
<li
class=""
class="ant-tree-treenode-switcher-open"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -621,7 +784,7 @@ exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
data-expanded="true"
>
<li
class=""
class="ant-tree-treenode-switcher-open"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -640,7 +803,7 @@ exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -659,7 +822,7 @@ exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -680,7 +843,7 @@ exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
</ul>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher_close"
@ -699,7 +862,7 @@ exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -720,7 +883,7 @@ exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
</ul>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher_close"
@ -739,7 +902,7 @@ exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -762,12 +925,12 @@ exports[`renders ./components/tree/demo/draggable.md correctly 1`] = `
exports[`renders ./components/tree/demo/dynamic.md correctly 1`] = `
<ul
class="ant-tree"
class="ant-tree ant-tree-icon-hide"
role="tree-node"
unselectable="on"
>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher_close"
@ -784,7 +947,7 @@ exports[`renders ./components/tree/demo/dynamic.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher_close"
@ -822,12 +985,12 @@ exports[`renders ./components/tree/demo/dynamic.md correctly 1`] = `
exports[`renders ./components/tree/demo/line.md correctly 1`] = `
<ul
class="ant-tree ant-tree-show-line"
class="ant-tree ant-tree-icon-hide ant-tree-show-line"
role="tree-node"
unselectable="on"
>
<li
class=""
class="ant-tree-treenode-switcher-open"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -847,7 +1010,7 @@ exports[`renders ./components/tree/demo/line.md correctly 1`] = `
data-expanded="true"
>
<li
class=""
class="ant-tree-treenode-switcher-open"
>
<span
class="ant-tree-switcher ant-tree-switcher_open"
@ -867,7 +1030,7 @@ exports[`renders ./components/tree/demo/line.md correctly 1`] = `
data-expanded="true"
>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -884,7 +1047,7 @@ exports[`renders ./components/tree/demo/line.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -901,7 +1064,7 @@ exports[`renders ./components/tree/demo/line.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"
@ -920,7 +1083,7 @@ exports[`renders ./components/tree/demo/line.md correctly 1`] = `
</ul>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher_close"
@ -937,7 +1100,7 @@ exports[`renders ./components/tree/demo/line.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher_close"
@ -978,12 +1141,12 @@ exports[`renders ./components/tree/demo/search.md correctly 1`] = `
</span>
</span>
<ul
class="ant-tree"
class="ant-tree ant-tree-icon-hide"
role="tree-node"
unselectable="on"
>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher_close"
@ -1005,7 +1168,7 @@ exports[`renders ./components/tree/demo/search.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher_close"
@ -1027,7 +1190,7 @@ exports[`renders ./components/tree/demo/search.md correctly 1`] = `
</span>
</li>
<li
class=""
class="ant-tree-treenode-switcher-close"
>
<span
class="ant-tree-switcher ant-tree-switcher-noop"

File diff suppressed because it is too large Load Diff

View File

@ -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 (
<DirectoryTree {...props}>
<TreeNode key="0-0">
<TreeNode key="0-0-0" />
<TreeNode key="0-0-1" />
</TreeNode>
<TreeNode key="0-1">
<TreeNode key="0-1-0" />
<TreeNode key="0-1-1" />
</TreeNode>
</DirectoryTree>
);
}
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;
});
});

View File

@ -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(
<Tree>
<TreeNode key="0-0">
<TreeNode key="0-0-0" />
<TreeNode key="0-0-1" />
</TreeNode>
<TreeNode key="0-1">
<TreeNode key="0-1-0" />
<TreeNode key="0-1-1" />
</TreeNode>
<TreeNode key="0-2">
<TreeNode key="0-2-0">
<TreeNode key="0-2-0-0" />
<TreeNode key="0-2-0-1" />
<TreeNode key="0-2-0-2" />
</TreeNode>
</TreeNode>
</Tree>
);
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());
});
});

View File

@ -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 (
<DirectoryTree
multiple
defaultExpandAll
onSelect={this.onSelect}
onExpand={this.onExpand}
>
<TreeNode title="parent 0" key="0-0">
<TreeNode title="leaf 0-0" key="0-0-0" isLeaf />
<TreeNode title="leaf 0-1" key="0-0-1" isLeaf />
</TreeNode>
<TreeNode title="parent 1" key="0-1">
<TreeNode title="leaf 1-0" key="0-1-0" isLeaf />
<TreeNode title="leaf 1-1" key="0-1-1" isLeaf />
</TreeNode>
</DirectoryTree>
);
}
}
ReactDOM.render(<Demo />, mountNode);
````

View File

@ -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`:

View File

@ -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<AntTreeNodeProps, {}> {}
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<any>;
}
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<any>;
/** 点击复选框触发 */
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<any>;
/** 响应右键点击 */
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<TreeProps, any> {
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 (
<RcTree
{...props}
className={className}
checkable={checkable ? <span className={`${prefixCls}-checkbox-inner`} /> : checkable}
>
{this.props.children}
</RcTree>
);
}
}
export default Tree;

View File

@ -56,6 +56,13 @@ subtitle: 树形控件
| selectable | 设置节点是否可被选中 | boolean | true |
| title | 标题 | string\|ReactNode | '---' |
### DirectoryTree props
| 参数 | 说明 | 类型 | 默认值 |
| --- | --- | --- | --- |
| expandAction | 目录展开逻辑,可选 `false` `'click'` `'doubleClick'` | string | click |
## 注意
`3.4.0` 之前:

View File

@ -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;
}
}
}
}
}
}
}

View File

@ -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;
}
}
}
}

56
components/tree/util.ts Normal file
View File

@ -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;
}

View File

@ -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",

View File

@ -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*';