feat: update tree and vc-tree

pull/225/head
tangjinzhou 2018-09-26 22:57:01 +08:00
parent 2fb6667593
commit 3dc6b0745c
18 changed files with 1622 additions and 856 deletions

View File

@ -0,0 +1,207 @@
import omit from 'omit.js'
import debounce from 'lodash/debounce'
import PropTypes from '../_util/vue-types'
import { conductExpandParent, convertTreeToEntities } from '../vc-tree/src/util'
import Tree, { TreeProps } from './Tree'
import { calcRangeKeys, getFullKeyList } from './util'
import Icon from '../icon'
import { initDefaultProps, getOptionProps } from '../_util/props-util'
// export type ExpandAction = false | 'click' | 'doubleClick';
// export interface DirectoryTreeProps extends TreeProps {
// expandAction?: ExpandAction;
// }
// export interface DirectoryTreeState {
// expandedKeys?: string[];
// selectedKeys?: string[];
// }
function getIcon (h, props) {
const { isLeaf, expanded } = props
if (isLeaf) {
return <Icon type='file' />
}
return <Icon type={expanded ? 'folder-open' : 'folder'} />
}
export default {
name: 'ADirectoryTree',
model: {
prop: 'checkedKeys',
event: 'check',
},
props: initDefaultProps({ ...TreeProps(), expandAction: PropTypes.oneOf([false, 'click', 'doubleClick']) }, {
prefixCls: 'ant-tree',
showIcon: true,
expandAction: 'click',
}),
// state: DirectoryTreeState;
// onDebounceExpand: (event, node: AntTreeNode) => void;
// // Shift click usage
// lastSelectedKey?: string;
// cachedSelectedKeys?: string[];
data () {
const props = getOptionProps(this)
const { defaultExpandAll, defaultExpandParent, expandedKeys, defaultExpandedKeys } = props
const { keyEntities } = convertTreeToEntities(this.$slots.default)
const state = {}
// Selected keys
state._selectedKeys = props.selectedKeys || props.defaultSelectedKeys || []
// Expanded keys
if (defaultExpandAll) {
state._expandedKeys = getFullKeyList(props.children)
} else if (defaultExpandParent) {
state._expandedKeys = conductExpandParent(expandedKeys || defaultExpandedKeys, keyEntities)
} else {
state._expandedKeys = defaultExpandedKeys
}
this.onDebounceExpand = debounce(this.expandFolderNode, 200, {
leading: true,
})
return {
_selectedKeys: [],
_expandedKeys: [],
...state,
}
},
watch: {
expandedKeys (val) {
this.setState({ _expandedKeys: val })
},
selectedKeys (val) {
this.setState({ _selectedKeys: val })
},
},
methods: {
onExpand (expandedKeys, info) {
this.setUncontrolledState({ _expandedKeys: expandedKeys })
this.$emit('expand', expandedKeys, info)
return undefined
},
onClick (event, node) {
const { expandAction } = this.$props
// Expand the tree
if (expandAction === 'click') {
this.onDebounceExpand(event, node)
}
this.$emit('click', event, node)
},
onDoubleClick (event, node) {
const { expandAction } = this.$props
// Expand the tree
if (expandAction === 'doubleClick') {
this.onDebounceExpand(event, node)
}
this.$emit('doubleclick', event, node)
},
onSelect (keys, event) {
const { multiple } = this.$props
const children = this.$slots.default || []
const { _expandedKeys: expandedKeys = [], _selectedKeys: selectedKeys = [] } = this.$data
const { node, nativeEvent } = event
const { eventKey = '' } = node
const newState = {}
// Windows / Mac single pick
const ctrlPick = nativeEvent.ctrlKey || nativeEvent.metaKey
const shiftPick = 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
this.$emit('select', newSelectedKeys, event)
this.setUncontrolledState(newState)
},
expandFolderNode (event, node) {
const { _expandedKeys: expandedKeys = [] } = this.$data
const { eventKey = '', expanded, isLeaf } = node
if (isLeaf || event.shiftKey || event.metaKey || event.ctrlKey) {
return
}
const newExpandedKeys = 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,
})
this.$emit('expand', newExpandedKeys, {
expanded: !expanded,
node,
nativeEvent: event.nativeEvent,
})
},
setUncontrolledState (state) {
const newState = omit(state, Object.keys(getOptionProps(this)).map(p => `_${p}`))
if (Object.keys(newState).length) {
this.setState(newState)
}
},
},
render () {
const { prefixCls, ...props } = getOptionProps(this)
const { _expandedKeys: expandedKeys, _selectedKeys: selectedKeys } = this.$data
const treeProps = {
props: {
icon: getIcon,
...props,
prefixCls,
expandedKeys,
selectedKeys,
},
class: `${prefixCls}-directory`,
select: this.onSelect,
click: this.onClick,
doubleclick: this.onDoubleClick,
expand: this.onExpand,
}
return (
<Tree {...treeProps}>{this.$slots.default}</Tree>
)
},
}

162
components/tree/Tree.jsx Normal file
View File

@ -0,0 +1,162 @@
import warning from 'warning'
import { Tree as VcTree, TreeNode } from '../vc-tree'
import animation from '../_util/openAnimation'
import PropTypes from '../_util/vue-types'
import { initDefaultProps, getOptionProps } from '../_util/props-util'
function TreeProps () {
return {
showLine: PropTypes.bool,
/** 是否支持多选 */
multiple: PropTypes.bool,
/** 是否自动展开父节点 */
autoExpandParent: PropTypes.bool,
/** checkable状态下节点选择完全受控父子节点选中状态不再关联*/
checkStrictly: PropTypes.bool,
/** 是否支持选中 */
checkable: PropTypes.bool,
/** 是否禁用树 */
disabled: PropTypes.bool,
/** 默认展开所有树节点 */
defaultExpandAll: PropTypes.bool,
/** 默认展开对应树节点 */
defaultExpandParent: PropTypes.bool,
/** 默认展开指定的树节点 */
defaultExpandedKeys: PropTypes.arrayOf(String),
/** (受控)展开指定的树节点 */
expandedKeys: PropTypes.arrayOf(String),
/** (受控)选中复选框的树节点 */
checkedKeys: PropTypes.oneOfType(
[
PropTypes.arrayOf(PropTypes.string),
PropTypes.shape({
checked: PropTypes.arrayOf(String),
halfChecked: PropTypes.arrayOf(String),
}).loose,
]
),
/** 默认选中复选框的树节点 */
defaultCheckedKeys: PropTypes.arrayOf(String),
/** (受控)设置选中的树节点 */
selectedKeys: PropTypes.arrayOf(String),
/** 默认选中的树节点 */
defaultSelectedKeys: PropTypes.arrayOf(String),
selectable: PropTypes.bool,
/** 展开/收起节点时触发 */
// onExpand: (expandedKeys: string[], info: AntTreeNodeExpandedEvent) => void | PromiseLike<any>,
/** 点击复选框触发 */
// onCheck: (checkedKeys: string[] | { checked: string[]; halfChecked: 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: PropTypes.func,
/** 异步加载数据 */
loadData: PropTypes.func,
loadedKeys: PropTypes.arrayOf(String),
// onLoaded: (loadedKeys: string[], info: { event: 'load', node: AntTreeNode; }) => void,
/** 响应右键点击 */
// onRightClick: (options: AntTreeNodeMouseEvent) => void,
/** 设置节点可拖拽IE>8*/
draggable: PropTypes.bool,
// /** */
// onDragStart: (options: AntTreeNodeMouseEvent) => void,
// /** dragenter */
// onDragEnter: (options: AntTreeNodeMouseEvent) => void,
// /** dragover */
// onDragOver: (options: AntTreeNodeMouseEvent) => void,
// /** dragleave */
// onDragLeave: (options: AntTreeNodeMouseEvent) => void,
// /** drop */
// onDrop: (options: AntTreeNodeMouseEvent) => void,
showIcon: PropTypes.bool,
icon: PropTypes.func,
prefixCls: PropTypes.string,
filterTreeNode: PropTypes.func,
openAnimation: PropTypes.any,
treeNodes: PropTypes.array,
treeData: PropTypes.array,
}
}
export { TreeProps }
export default {
name: 'ATree',
model: {
prop: 'checkedKeys',
event: 'check',
},
props: initDefaultProps(TreeProps(), {
prefixCls: 'ant-tree',
checkable: false,
showIcon: false,
openAnimation: {
on: animation,
props: { appear: null },
},
}),
created () {
warning(
!('treeNodes' in getOptionProps(this)),
'`treeNodes` is deprecated. please use treeData instead.'
)
},
TreeNode,
methods: {
updataTreeData (treeData) {
const { $slots, $scopedSlots } = this
return treeData.map((item) => {
const { children, on = {}, slots = {}, scopedSlots = {}, key, class: cls, style, ...restProps } = item
const treeNodeProps = {
...restProps,
icon: restProps.icon ||
$slots[slots.icon] ||
($scopedSlots[scopedSlots.icon] && $scopedSlots[scopedSlots.icon]),
title: restProps.title ||
$slots[slots.title] ||
($scopedSlots[scopedSlots.title] && $scopedSlots[scopedSlots.title])(item),
dataRef: item,
on,
key,
class: cls,
style,
}
if (children) {
return { ...treeNodeProps, children: this.updataTreeData(children) }
}
return treeNodeProps
})
},
},
render () {
const props = getOptionProps(this)
const { prefixCls, showIcon, treeNodes } = props
const checkable = props.checkable
let treeData = props.treeData || treeNodes
if (treeData) {
treeData = this.updataTreeData(treeData)
}
const vcTreeProps = {
props: {
...props,
checkable: checkable ? <span class={`${prefixCls}-checkbox-inner`} /> : checkable,
children: this.$slots.default || [],
__propsSymbol__: Symbol(),
},
on: {
...this.$listeners,
},
class: !showIcon && `${prefixCls}-icon-hide`,
}
if (treeData) {
vcTreeProps.props.treeData = treeData
}
return (
<VcTree {...vcTreeProps} />
)
},
}

View File

@ -18,7 +18,7 @@ basic controlled example
v-model="checkedKeys"
@select="onSelect"
:selectedKeys="selectedKeys"
:treeNodes="treeData"
:treeData="treeData"
/>
</template>
<script>
@ -75,7 +75,7 @@ export default {
},
methods: {
onExpand (expandedKeys) {
console.log('onExpand', arguments)
console.log('onExpand', expandedKeys)
// if not set autoExpandParent to false, if children expanded, parent can not collapse.
// or, you can remove all expanded children keys.
this.expandedKeys = expandedKeys

View File

@ -12,7 +12,7 @@ The most basic usage, tell you how to use checkable, selectable, disabled, defau
<template>
<a-tree
checkable
:treeNodes="treeData"
:treeData="treeData"
:defaultExpandedKeys="['0-0-0', '0-0-1']"
:defaultSelectedKeys="['0-0-0', '0-0-1']"
:defaultCheckedKeys="['0-0-0', '0-0-1']"

View File

@ -11,7 +11,7 @@ You can customize icons for different nodes.
```html
<template>
<a-tree
:treeNodes="treeData"
:treeData="treeData"
showIcon
defaultExpandAll
:defaultSelectedKeys="['0-0-0']"
@ -32,7 +32,7 @@ const treeData = [{
},
children: [
{ title: 'leaf', key: '0-0-0', slots: { icon: 'meh' }},
{ title: 'leaf', key: '0-1-1', scopedSlots: { icon: 'custom' }}],
{ title: 'leaf', key: '0-0-1', scopedSlots: { icon: 'custom' }}],
}]
export default {

View File

@ -16,7 +16,7 @@ Drag treeNode to insert after the other treeNode or insert into the other parent
draggable
@dragenter="onDragEnter"
@drop="onDrop"
:treeNodes="gData"
:treeData="gData"
/>
</template>

View File

@ -12,7 +12,7 @@ To load data asynchronously when click to expand a treeNode.
<template>
<a-tree
:loadData="onLoadData"
:treeNodes="treeData"
:treeData="treeData"
/>
</template>

View File

@ -1,183 +1,17 @@
import VcTree, { TreeNode } from '../vc-tree'
import animation from '../_util/openAnimation'
import PropTypes from '../_util/vue-types'
import { initDefaultProps, getOptionProps } from '../_util/props-util'
// export interface AntTreeNodeProps {
// disabled: PropTypes.bool,
// disableCheckbox: PropTypes.bool,
// title?: string | React.ReactNode;
// key?: string;
// isLeaf: PropTypes.bool,
// children?: React.ReactNode;
// }
// export interface AntTreeNode extends React.Component<AntTreeNodeProps, {}> {}
// export interface AntTreeNodeEvent {
// event: 'check' | 'select';
// node: AntTreeNode;
// checked: PropTypes.bool,
// checkedNodes?: Array<AntTreeNode>;
// selected: PropTypes.bool,
// selectedNodes?: Array<AntTreeNode>;
// }
// export interface AntTreeNodeMouseEvent {
// node: AntTreeNode;
// event: React.MouseEventHandler<any>;
// }
export const TreeProps = () => ({
treeNodes: PropTypes.array,
showLine: PropTypes.bool,
/** 是否支持多选 */
multiple: PropTypes.boolean,
/** 是否自动展开父节点 */
autoExpandParent: PropTypes.boolean,
/** checkable状态下节点选择完全受控父子节点选中状态不再关联*/
checkStrictly: PropTypes.bool,
/** 是否支持选中 */
checkable: PropTypes.bool,
/** 默认展开所有树节点 */
defaultExpandAll: PropTypes.bool,
/** 默认展开指定的树节点 */
defaultExpandedKeys: PropTypes.arrayOf(PropTypes.string),
/** (受控)展开指定的树节点 */
expandedKeys: PropTypes.arrayOf(PropTypes.string),
/** (受控)选中复选框的树节点 */
checkedKeys: PropTypes.oneOfType(
[
PropTypes.arrayOf(PropTypes.string),
PropTypes.shape({
checked: PropTypes.arrayOf(String),
halfChecked: PropTypes.arrayOf(String),
}).loose,
]
),
/** 默认选中复选框的树节点 */
defaultCheckedKeys: PropTypes.arrayOf(PropTypes.string),
/** (受控)设置选中的树节点 */
selectedKeys: PropTypes.arrayOf(PropTypes.string),
/** 默认选中的树节点 */
defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
/** 展开/收起节点时触发 */
// onExpand?: (expandedKeys: Array<string>, info: { node: AntTreeNode, expanded: boolean }) => void | PromiseLike<any>;
/** 点击复选框触发 */
// onCheck?: (checkedKeys: Array<string>, e: AntTreeNodeEvent) => void;
/** 点击树节点触发 */
// onSelect?: (selectedKeys: Array<string>, e: AntTreeNodeEvent) => void;
/** filter some AntTreeNodes as you need. it should return true */
filterAntTreeNode: PropTypes.func,
/** 异步加载数据 */
loadData: PropTypes.func,
/** 响应右键点击 */
// onRightClick?: (options: AntTreeNodeMouseEvent) => void;
/** 设置节点可拖拽IE>8*/
draggable: PropTypes.bool,
// /** */
// onDragStart?: (options: AntTreeNodeMouseEvent) => void;
// /** dragenter */
// onDragEnter?: (options: AntTreeNodeMouseEvent) => void;
// /** dragover */
// onDragOver?: (options: AntTreeNodeMouseEvent) => void;
// /** dragleave */
// onDragLeave?: (options: AntTreeNodeMouseEvent) => void;
// /** drop */
// onDrop?: (options: AntTreeNodeMouseEvent) => void;
prefixCls: PropTypes.string,
filterTreeNode: PropTypes.func,
showIcon: PropTypes.bool,
openAnimation: PropTypes.any,
})
const Tree = {
name: 'ATree',
TreeNode: { ...TreeNode, name: 'ATreeNode' },
props: initDefaultProps(TreeProps(), {
prefixCls: 'ant-tree',
checkable: false,
showIcon: false,
openAnimation: animation,
}),
model: {
prop: 'checkedKeys',
event: 'check',
},
methods: {
handleCheck (checkedKeys, e) {
this.$emit('check', checkedKeys, e)
},
handelSelect (selectedKeys, e) {
this.$emit('select', selectedKeys, e)
this.$emit('update:select', selectedKeys)
},
handleExpand (expandedKeys, info) {
this.$emit('expand', expandedKeys, info)
this.$emit('update:expand', expandedKeys)
},
renderTreeNodes (data = []) {
const { $slots, $scopedSlots } = this
return data.map((item) => {
const { children, on = {}, slots = {}, scopedSlots = {}, key, class: cls, style, ...restProps } = item
const treeNodeProps = {
props: {
...restProps,
icon: restProps.icon ||
$slots[slots.icon] ||
($scopedSlots[scopedSlots.icon] && $scopedSlots[scopedSlots.icon]),
title: restProps.title ||
$slots[slots.title] ||
($scopedSlots[scopedSlots.title] && $scopedSlots[scopedSlots.title])(item),
dataRef: item,
},
on,
key,
class: cls,
style,
}
if (children) {
return (
<TreeNode {...treeNodeProps}>
{this.renderTreeNodes(children)}
</TreeNode>
)
}
return <TreeNode {...treeNodeProps} />
})
},
},
render () {
const props = getOptionProps(this)
const { prefixCls, checkable, treeNodes, ...restProps } = props
const { handelSelect, handleCheck, handleExpand, renderTreeNodes } = this
const vcTreeProps = {
props: {
...restProps,
prefixCls,
checkable: checkable ? <span class={`${prefixCls}-checkbox-inner`} /> : checkable,
},
on: {
...this.$listeners,
check: handleCheck,
select: handelSelect,
expand: handleExpand,
},
}
return (
<VcTree {...vcTreeProps}>
{treeNodes ? renderTreeNodes(treeNodes) : this.$slots.default}
</VcTree>
)
},
}
import Tree from './Tree'
import DirectoryTree from './DirectoryTree'
// export {
// TreeProps,
// } from './Tree'
Tree.TreeNode.name = 'ATreeNode'
Tree.DirectoryTree = DirectoryTree
/* istanbul ignore next */
Tree.install = function (Vue) {
Vue.component(Tree.name, Tree)
Vue.component(Tree.TreeNode.name, Tree.TreeNode)
Vue.component(DirectoryTree.name, DirectoryTree)
}
export default Tree

View File

@ -0,0 +1,183 @@
import VcTree, { TreeNode } from '../vc-tree'
import animation from '../_util/openAnimation'
import PropTypes from '../_util/vue-types'
import { initDefaultProps, getOptionProps } from '../_util/props-util'
// export interface AntTreeNodeProps {
// disabled: PropTypes.bool,
// disableCheckbox: PropTypes.bool,
// title?: string | React.ReactNode;
// key?: string;
// isLeaf: PropTypes.bool,
// children?: React.ReactNode;
// }
// export interface AntTreeNode extends React.Component<AntTreeNodeProps, {}> {}
// export interface AntTreeNodeEvent {
// event: 'check' | 'select';
// node: AntTreeNode;
// checked: PropTypes.bool,
// checkedNodes?: Array<AntTreeNode>;
// selected: PropTypes.bool,
// selectedNodes?: Array<AntTreeNode>;
// }
// export interface AntTreeNodeMouseEvent {
// node: AntTreeNode;
// event: React.MouseEventHandler<any>;
// }
export const TreeProps = () => ({
treeNodes: PropTypes.array,
showLine: PropTypes.bool,
/** 是否支持多选 */
multiple: PropTypes.boolean,
/** 是否自动展开父节点 */
autoExpandParent: PropTypes.boolean,
/** checkable状态下节点选择完全受控父子节点选中状态不再关联*/
checkStrictly: PropTypes.bool,
/** 是否支持选中 */
checkable: PropTypes.bool,
/** 默认展开所有树节点 */
defaultExpandAll: PropTypes.bool,
/** 默认展开指定的树节点 */
defaultExpandedKeys: PropTypes.arrayOf(PropTypes.string),
/** (受控)展开指定的树节点 */
expandedKeys: PropTypes.arrayOf(PropTypes.string),
/** (受控)选中复选框的树节点 */
checkedKeys: PropTypes.oneOfType(
[
PropTypes.arrayOf(PropTypes.string),
PropTypes.shape({
checked: PropTypes.arrayOf(String),
halfChecked: PropTypes.arrayOf(String),
}).loose,
]
),
/** 默认选中复选框的树节点 */
defaultCheckedKeys: PropTypes.arrayOf(PropTypes.string),
/** (受控)设置选中的树节点 */
selectedKeys: PropTypes.arrayOf(PropTypes.string),
/** 默认选中的树节点 */
defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
/** 展开/收起节点时触发 */
// onExpand?: (expandedKeys: Array<string>, info: { node: AntTreeNode, expanded: boolean }) => void | PromiseLike<any>;
/** 点击复选框触发 */
// onCheck?: (checkedKeys: Array<string>, e: AntTreeNodeEvent) => void;
/** 点击树节点触发 */
// onSelect?: (selectedKeys: Array<string>, e: AntTreeNodeEvent) => void;
/** filter some AntTreeNodes as you need. it should return true */
filterAntTreeNode: PropTypes.func,
/** 异步加载数据 */
loadData: PropTypes.func,
/** 响应右键点击 */
// onRightClick?: (options: AntTreeNodeMouseEvent) => void;
/** 设置节点可拖拽IE>8*/
draggable: PropTypes.bool,
// /** */
// onDragStart?: (options: AntTreeNodeMouseEvent) => void;
// /** dragenter */
// onDragEnter?: (options: AntTreeNodeMouseEvent) => void;
// /** dragover */
// onDragOver?: (options: AntTreeNodeMouseEvent) => void;
// /** dragleave */
// onDragLeave?: (options: AntTreeNodeMouseEvent) => void;
// /** drop */
// onDrop?: (options: AntTreeNodeMouseEvent) => void;
prefixCls: PropTypes.string,
filterTreeNode: PropTypes.func,
showIcon: PropTypes.bool,
openAnimation: PropTypes.any,
})
const Tree = {
name: 'ATree',
TreeNode: { ...TreeNode, name: 'ATreeNode' },
props: initDefaultProps(TreeProps(), {
prefixCls: 'ant-tree',
checkable: false,
showIcon: false,
openAnimation: animation,
}),
model: {
prop: 'checkedKeys',
event: 'check',
},
methods: {
handleCheck (checkedKeys, e) {
this.$emit('check', checkedKeys, e)
},
handelSelect (selectedKeys, e) {
this.$emit('select', selectedKeys, e)
this.$emit('update:select', selectedKeys)
},
handleExpand (expandedKeys, info) {
this.$emit('expand', expandedKeys, info)
this.$emit('update:expand', expandedKeys)
},
renderTreeNodes (data = []) {
const { $slots, $scopedSlots } = this
return data.map((item) => {
const { children, on = {}, slots = {}, scopedSlots = {}, key, class: cls, style, ...restProps } = item
const treeNodeProps = {
props: {
...restProps,
icon: restProps.icon ||
$slots[slots.icon] ||
($scopedSlots[scopedSlots.icon] && $scopedSlots[scopedSlots.icon]),
title: restProps.title ||
$slots[slots.title] ||
($scopedSlots[scopedSlots.title] && $scopedSlots[scopedSlots.title])(item),
dataRef: item,
},
on,
key,
class: cls,
style,
}
if (children) {
return (
<TreeNode {...treeNodeProps}>
{this.renderTreeNodes(children)}
</TreeNode>
)
}
return <TreeNode {...treeNodeProps} />
})
},
},
render () {
const props = getOptionProps(this)
const { prefixCls, checkable, treeNodes, ...restProps } = props
const { handelSelect, handleCheck, handleExpand, renderTreeNodes } = this
const vcTreeProps = {
props: {
...restProps,
prefixCls,
checkable: checkable ? <span class={`${prefixCls}-checkbox-inner`} /> : checkable,
},
on: {
...this.$listeners,
check: handleCheck,
select: handelSelect,
expand: handleExpand,
},
}
return (
<VcTree {...vcTreeProps}>
{treeNodes ? renderTreeNodes(treeNodes) : this.$slots.default}
</VcTree>
)
},
}
/* istanbul ignore next */
Tree.install = function (Vue) {
Vue.component(Tree.name, Tree)
Vue.component(Tree.TreeNode.name, Tree.TreeNode)
}
export default Tree

72
components/tree/util.js Normal file
View File

@ -0,0 +1,72 @@
import { getNodeChildren, convertTreeToEntities } from '../vc-tree/src/util'
const Record = {
None: 'node',
Start: 'start',
End: 'end',
}
// TODO: Move this logic into `rc-tree`
function traverseNodesKey (rootChildren, callback) {
const nodeList = getNodeChildren(rootChildren) || []
function processNode (node) {
const { key, props: { children }} = node
if (callback(key) !== false) {
traverseNodesKey(children, callback)
}
}
nodeList.forEach(processNode)
}
export function getFullKeyList (children) {
const { keyEntities } = convertTreeToEntities(children)
return Object.keys(keyEntities)
}
/** 计算选中范围只考虑expanded情况以优化性能 */
export function calcRangeKeys (rootChildren, expandedKeys, startKey, endKey) {
const keys = []
let record = Record.None
if (startKey && startKey === endKey) {
return [startKey]
}
if (!startKey || !endKey) {
return []
}
function matchKey (key) {
return key === startKey || key === endKey
}
traverseNodesKey(rootChildren, (key) => {
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 true
})
return keys
}

View File

@ -161,7 +161,7 @@
>span:not(.@{treePrefixCls}-switcher),
>a,
>a span {
color: #ccc;
color: #767676;
cursor: not-allowed;
}
}

View File

@ -2,8 +2,7 @@
/* eslint no-console:0 */
import Tree, { TreeNode } from '../index'
import '../assets/index.less'
import { gData,
/* filterParentPosition, getFilterExpandedKeys,*/ getRadioSelectKeys } from './util'
import { gData, getRadioSelectKeys } from './util'
import '../../vc-dialog/assets/index.less'
import Modal from '../../vc-dialog'
import BaseMixin from '../../_util/BaseMixin'
@ -63,7 +62,7 @@ export default {
onRbSelect (selectedKeys, info) {
let _selectedKeys = selectedKeys
if (info.selected) {
_selectedKeys = getRadioSelectKeys(gData, selectedKeys, info.node.props.eventKey)
_selectedKeys = getRadioSelectKeys(gData, selectedKeys, info.node.eventKey)
}
this.setState({
selectedKeys: _selectedKeys,

View File

@ -5,6 +5,24 @@ import Tree, { TreeNode } from '../index'
import '../assets/index.less'
import './basic.less'
const treeData = [
{ key: '0-0', title: 'parent 1', children:
[
{ key: '0-0-0', title: 'parent 1-1', children:
[
{ key: '0-0-0-0', title: 'parent 1-1-0' },
],
},
{ key: '0-0-1', title: 'parent 1-2', children:
[
{ key: '0-0-1-0', title: 'parent 1-2-0', disableCheckbox: true },
{ key: '0-0-1-1', title: 'parent 1-2-1' },
],
},
],
},
]
export default {
props: {
keys: PropTypes.array.def(['0-0-0-0']),
@ -15,7 +33,6 @@ export default {
defaultExpandedKeys: keys,
defaultSelectedKeys: keys,
defaultCheckedKeys: keys,
switchIt: true,
}
},
methods: {
@ -46,15 +63,20 @@ export default {
},
render () {
const customLabel = (<span class='cus-label'>
<span>operations: </span>
<span style={{ color: 'blue' }} onClick={this.onEdit}>Edit</span>&nbsp;
<label onClick={(e) => e.stopPropagation()}><input type='checkbox' /> checked</label> &nbsp;
<span style={{ color: 'red' }} onClick={this.onDel}>Delete</span>
</span>)
const customLabel = (
<span class='cus-label'>
<span>operations: </span>
<span style={{ color: 'blue' }} onClick={this.onEdit}>Edit</span>&nbsp;
<label onClick={(e) => e.stopPropagation()}>
<input type='checkbox' /> checked
</label>
&nbsp;
<span style={{ color: '#EB0000' }} onClick={this.onDel}>Delete</span>
</span>
)
return (<div style={{ margin: '0 20px' }}>
<h2>simple</h2>
<Tree
{/* <Tree
class='myCls' showLine checkable defaultExpandAll
defaultExpandedKeys={this.defaultExpandedKeys}
onExpand={this.onExpand}
@ -76,7 +98,7 @@ export default {
<TreeNode title='parent 1-2-1' key='0-0-2-1' />
</TreeNode>
</TreeNode>
</Tree>
</Tree> */}
<h2>Check on Click TreeNode</h2>
<Tree
@ -90,17 +112,8 @@ export default {
defaultCheckedKeys={this.defaultCheckedKeys}
onSelect={this.onSelect}
onCheck={this.onCheck}
>
<TreeNode title='parent 1' key='0-0'>
<TreeNode title='parent 1-1' key='0-0-0'>
<TreeNode title='parent 1-1-0' key='0-0-0-0' />
</TreeNode>
<TreeNode title='parent 1-2' key='0-0-1'>
<TreeNode title='parent 1-2-0' key='0-0-1-0' disableCheckbox />
<TreeNode title='parent 1-2-1' key='0-0-1-1' />
</TreeNode>
</TreeNode>
</Tree>
treeData={treeData}
/>
</div>)
},
}

View File

@ -4,12 +4,16 @@ import warning from 'warning'
import { initDefaultProps, getOptionProps, getSlots } from '../../_util/props-util'
import { cloneElement } from '../../_util/vnode'
import BaseMixin from '../../_util/BaseMixin'
import proxyComponent from '../../_util/proxyComponent'
import {
traverseTreeNodes, getStrictlyValue,
getFullKeyList, getPosition, getDragNodesKeys,
calcExpandedKeys, calcSelectedKeys,
calcCheckedKeys, calcDropPosition,
arrAdd, arrDel, posToArr, mapChildren,
convertTreeToEntities, convertDataToTree,
getPosition, getDragNodesKeys,
parseCheckedKeys,
conductExpandParent, calcSelectedKeys,
calcDropPosition,
arrAdd, arrDel, posToArr,
mapChildren, conductCheck,
warnOnlyTreeNode,
} from './util'
/**
@ -22,6 +26,9 @@ const Tree = {
mixins: [BaseMixin],
props: initDefaultProps({
prefixCls: PropTypes.string,
tabIndex: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
children: PropTypes.any,
treeData: PropTypes.array, // Generate treeNode by children
showLine: PropTypes.bool,
showIcon: PropTypes.bool,
icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
@ -63,7 +70,8 @@ const Tree = {
filterTreeNode: PropTypes.func,
openTransitionName: PropTypes.string,
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
children: PropTypes.any,
switcherIcon: PropTypes.any,
_propsSymbol: PropTypes.any,
}, {
prefixCls: 'rc-tree',
showLine: false,
@ -82,53 +90,31 @@ const Tree = {
defaultSelectedKeys: [],
}),
// static childContextTypes = contextTypes;
data () {
const props = getOptionProps(this)
const {
defaultExpandAll,
defaultExpandParent,
defaultExpandedKeys,
defaultCheckedKeys,
defaultSelectedKeys,
expandedKeys,
} = props
const children = this.$slots.default
// Sync state with props
const { checkedKeys = [], halfCheckedKeys = [] } =
calcCheckedKeys(defaultCheckedKeys, props, children) || {}
const state = {
sSelectedKeys: calcSelectedKeys(defaultSelectedKeys, props),
sCheckedKeys: checkedKeys,
sHalfCheckedKeys: halfCheckedKeys,
}
if (defaultExpandAll) {
state.sExpandedKeys = getFullKeyList(children)
} else if (defaultExpandParent) {
state.sExpandedKeys = calcExpandedKeys(expandedKeys || defaultExpandedKeys, props, children)
} else {
state.sExpandedKeys = defaultExpandedKeys
}
// Cache for check status to optimize
this.checkedBatch = null
this.propsToStateMap = {
expandedKeys: 'sExpandedKeys',
selectedKeys: 'sSelectedKeys',
checkedKeys: 'sCheckedKeys',
halfCheckedKeys: 'sHalfCheckedKeys',
_posEntities: {},
_keyEntities: {},
_expandedKeys: [],
_selectedKeys: [],
_checkedKeys: [],
_halfCheckedKeys: [],
_loadedKeys: [],
_loadingKeys: [],
_treeNode: [],
_prevProps: null,
_dragOverNodeKey: '',
_dropPosition: null,
_dragNodesKeys: [],
}
return {
...state,
...this.getSyncProps(props),
dragOverNodeKey: '',
dropPosition: null,
dragNodesKeys: [],
sLoadedKeys: [],
sLoadingKeys: [],
// ...this.getSyncProps(props),
// dragOverNodeKey: '',
// dropPosition: null,
// dragNodesKeys: [],
// sLoadedKeys: [],
// sLoadingKeys: [],
...this.getDerivedStateFromProps(getOptionProps(this), state),
}
},
provide () {
@ -138,45 +124,110 @@ const Tree = {
},
watch: {
children (val) {
const { checkedKeys = [], halfCheckedKeys = [] } = calcCheckedKeys(this.checkedKeys || this.sCheckedKeys, this.$props, val) || {}
this.sCheckedKeys = checkedKeys
this.sHalfCheckedKeys = halfCheckedKeys
},
autoExpandParent (val) {
this.sExpandedKeys = val ? calcExpandedKeys(this.expandedKeys, this.$props, this.$slots.default) : this.expandedKeys
},
expandedKeys (val) {
this.sExpandedKeys = this.autoExpandParent ? calcExpandedKeys(val, this.$props, this.$slots.default) : val
},
selectedKeys (val) {
this.sSelectedKeys = calcSelectedKeys(val, this.$props, this.$slots.default)
},
checkedKeys (val) {
const { checkedKeys = [], halfCheckedKeys = [] } = calcCheckedKeys(val, this.$props, this.$slots.default) || {}
this.sCheckedKeys = checkedKeys
this.sHalfCheckedKeys = halfCheckedKeys
},
loadedKeys (val) {
this.sLoadedKeys = val
__propsSymbol__ () {
this.setState(this.getDerivedStateFromProps(getOptionProps(this), this.$data))
},
},
// componentWillReceiveProps (nextProps) {
// // React 16 will not trigger update if new state is null
// this.setState(this.getSyncProps(nextProps, this.props))
// },
methods: {
getDerivedStateFromProps (props, prevState) {
const { _prevProps } = prevState
const newState = {
_prevProps: { ...props },
}
function needSync (name) {
return (!_prevProps && name in props) || (_prevProps && _prevProps[name] !== props[name])
}
// ================== Tree Node ==================
let treeNode = null
// Check if `treeData` or `children` changed and save into the state.
if (needSync('treeData')) {
treeNode = convertDataToTree(this.$createElement, props.treeData)
} else if (needSync('children')) {
treeNode = props.children
}
// Tree support filter function which will break the tree structure in the vdm.
// We cache the treeNodes in state so that we can return the treeNode in event trigger.
if (treeNode) {
newState._treeNode = treeNode
// Calculate the entities data for quick match
const entitiesMap = convertTreeToEntities(treeNode)
newState._posEntities = entitiesMap.posEntities
newState._keyEntities = entitiesMap.keyEntities
}
const keyEntities = newState._keyEntities || prevState._keyEntities
// ================ expandedKeys =================
if (needSync('expandedKeys') || (_prevProps && needSync('autoExpandParent'))) {
newState._expandedKeys = (props.autoExpandParent || (!_prevProps && props.defaultExpandParent))
? conductExpandParent(props.expandedKeys, keyEntities) : props.expandedKeys
} else if (!_prevProps && props.defaultExpandAll) {
newState._expandedKeys = Object.keys(keyEntities)
} else if (!_prevProps && props.defaultExpandedKeys) {
newState._expandedKeys = (props.autoExpandParent || props.defaultExpandParent)
? conductExpandParent(props.defaultExpandedKeys, keyEntities) : props.defaultExpandedKeys
}
// ================ selectedKeys =================
if (props.selectable) {
if (needSync('selectedKeys')) {
newState._selectedKeys = calcSelectedKeys(props.selectedKeys, props)
} else if (!_prevProps && props.defaultSelectedKeys) {
newState._selectedKeys = calcSelectedKeys(props.defaultSelectedKeys, props)
}
}
// ================= checkedKeys =================
if (props.checkable) {
let checkedKeyEntity
if (needSync('checkedKeys')) {
checkedKeyEntity = parseCheckedKeys(props.checkedKeys) || {}
} else if (!_prevProps && props.defaultCheckedKeys) {
checkedKeyEntity = parseCheckedKeys(props.defaultCheckedKeys) || {}
} else if (treeNode) {
// If treeNode changed, we also need check it
checkedKeyEntity = {
checkedKeys: prevState._checkedKeys,
halfCheckedKeys: prevState._halfCheckedKeys,
}
}
if (checkedKeyEntity) {
let { checkedKeys = [], halfCheckedKeys = [] } = checkedKeyEntity
if (!props.checkStrictly) {
const conductKeys = conductCheck(checkedKeys, true, keyEntities)
checkedKeys = conductKeys.checkedKeys
halfCheckedKeys = conductKeys.halfCheckedKeys
}
newState._checkedKeys = checkedKeys
newState._halfCheckedKeys = halfCheckedKeys
}
}
// ================= loadedKeys ==================
if (needSync('loadedKeys')) {
newState._loadedKeys = props.loadedKeys
}
return newState
},
onNodeDragStart (event, node) {
const { sExpandedKeys } = this
const { _expandedKeys } = this.$data
const { eventKey } = node
const children = getSlots(node).default
this.dragNode = node
this.setState({
dragNodesKeys: getDragNodesKeys(children, node),
sExpandedKeys: arrDel(sExpandedKeys, eventKey),
_dragNodesKeys: getDragNodesKeys(children, node),
_expandedKeys: arrDel(_expandedKeys, eventKey),
})
this.__emit('dragstart', { event, node })
},
@ -189,10 +240,10 @@ const Tree = {
* But let's just keep it to avoid event trigger logic change.
*/
onNodeDragEnter (event, node) {
const { sExpandedKeys } = this
const { _expandedKeys: expandedKeys } = this.$data
const { pos, eventKey } = node
if (!this.dragNode) return
if (!this.dragNode || !node.$refs.selectHandle) return
const dropPosition = calcDropPosition(event, node)
@ -202,8 +253,8 @@ const Tree = {
dropPosition === 0
) {
this.setState({
dragOverNodeKey: '',
dropPosition: null,
_dragOverNodeKey: '',
_dropPosition: null,
})
return
}
@ -216,8 +267,8 @@ const Tree = {
setTimeout(() => {
// Update drag over node
this.setState({
dragOverNodeKey: eventKey,
dropPosition,
_dragOverNodeKey: eventKey,
_dropPosition: dropPosition,
})
// Side effect for delay drag
@ -228,9 +279,9 @@ const Tree = {
clearTimeout(this.delayedDragEnterLogic[key])
})
this.delayedDragEnterLogic[pos] = setTimeout(() => {
const newExpandedKeys = arrAdd(sExpandedKeys, eventKey)
const newExpandedKeys = arrAdd(expandedKeys, eventKey)
this.setState({
sExpandedKeys: newExpandedKeys,
_expandedKeys: newExpandedKeys,
})
this.__emit('dragenter', { event, node, expandedKeys: newExpandedKeys })
}, 400)
@ -238,42 +289,41 @@ const Tree = {
},
onNodeDragOver (event, node) {
const { eventKey } = node
const { _dragOverNodeKey, _dropPosition } = this.$data
// Update drag position
if (this.dragNode && eventKey === this.dragOverNodeKey) {
if (this.dragNode && eventKey === _dragOverNodeKey && node.$refs.selectHandle) {
const dropPosition = calcDropPosition(event, node)
if (dropPosition === this.dropPosition) return
if (dropPosition === _dropPosition) return
this.setState({
dropPosition,
_dropPosition,
})
}
this.__emit('dragover', { event, node })
},
onNodeDragLeave (event, node) {
this.setState({
dragOverNodeKey: '',
_dragOverNodeKey: '',
})
this.__emit('dragleave', { event, node })
},
onNodeDragEnd (event, node) {
this.setState({
dragOverNodeKey: '',
_dragOverNodeKey: '',
})
this.__emit('dragend', { event, node })
},
onNodeDrop (event, node) {
const { dragNodesKeys = [], dropPosition } = this
const { _dragNodesKeys = [], _dropPosition } = this.$data
const { eventKey, pos } = node
this.setState({
dragOverNodeKey: '',
dropNodeKey: eventKey,
_dragOverNodeKey: '',
})
if (dragNodesKeys.indexOf(eventKey) !== -1) {
if (_dragNodesKeys.indexOf(eventKey) !== -1) {
warning(false, 'Can not drop to dragNode(include it\'s children node)')
return
}
@ -284,11 +334,11 @@ const Tree = {
event,
node,
dragNode: this.dragNode,
dragNodesKeys: dragNodesKeys.slice(),
dropPosition: dropPosition + Number(posArr[posArr.length - 1]),
dragNodesKeys: _dragNodesKeys.slice(),
dropPosition: _dropPosition + Number(posArr[posArr.length - 1]),
}
if (dropPosition !== 0) {
if (_dropPosition !== 0) {
dropResult.dropToGap = true
}
this.__emit('drop', dropResult)
@ -303,10 +353,11 @@ const Tree = {
},
onNodeSelect (e, treeNode) {
const { sSelectedKeys, multiple, $slots: { default: children }} = this
let { _selectedKeys: selectedKeys } = this.$data
const { _keyEntities: keyEntities } = this.$data
const { multiple } = this.$props
const { selected, eventKey } = getOptionProps(treeNode)
const targetSelected = !selected
let selectedKeys = sSelectedKeys
// Update selected keys
if (!targetSelected) {
selectedKeys = arrDel(selectedKeys, eventKey)
@ -317,17 +368,14 @@ const Tree = {
}
// [Legacy] Not found related usage in doc or upper libs
// [Legacy] TODO: add optimize prop to skip node process
const selectedNodes = []
if (selectedKeys.length) {
traverseTreeNodes(children, ({ node, key }) => {
if (selectedKeys.indexOf(key) !== -1) {
selectedNodes.push(node)
}
})
}
const selectedNodes = selectedKeys.map(key => {
const entity = keyEntities[key]
if (!entity) return null
this.setUncontrolledState({ selectedKeys })
return entity.node
}).filter(node => node)
this.setUncontrolledState({ _selectedKeys: selectedKeys })
const eventObj = {
event: 'select',
@ -338,142 +386,100 @@ const Tree = {
}
this.__emit('select', selectedKeys, eventObj)
},
onNodeLoad (treeNode) {
const { loadData } = this.$props
const { sLoadedKeys = [], sLoadingKeys = [] } = this.$data
onNodeCheck (e, treeNode, checked) {
const { _keyEntities: keyEntities, _checkedKeys: oriCheckedKeys, _halfCheckedKeys: oriHalfCheckedKeys } = this.$data
const { checkStrictly } = this.$props
const { eventKey } = getOptionProps(treeNode)
if (!loadData || sLoadedKeys.indexOf(eventKey) !== -1 || sLoadingKeys.indexOf(eventKey) !== -1) {
return null
}
this.setState({
sLoadingKeys: arrAdd(sLoadingKeys, eventKey),
})
const promise = loadData(treeNode)
promise.then(() => {
const newLoadedKeys = arrAdd(this.sLoadedKeys, eventKey)
this.setUncontrolledState({
sLoadedKeys: newLoadedKeys,
})
this.setState({
sLoadingKeys: arrDel(this.sLoadingKeys, eventKey),
})
const eventObj = {
event: 'load',
node: treeNode,
}
this.__emit('load', newLoadedKeys, eventObj)
})
return promise
},
/**
* This will cache node check status to optimize update process.
* When Tree get trigger `onCheckConductFinished` will flush all the update.
*/
onBatchNodeCheck (key, checked, halfChecked, startNode) {
if (startNode) {
this.checkedBatch = {
treeNode: startNode,
checked,
list: [],
}
}
// This code should never called
if (!this.checkedBatch) {
this.checkedBatch = {
list: [],
}
warning(
false,
'Checked batch not init. This should be a bug. Please fire a issue.'
)
}
this.checkedBatch.list.push({ key, checked, halfChecked })
},
/**
* When top `onCheckConductFinished` called, will execute all batch update.
* And trigger `onCheck` event.
*/
onCheckConductFinished (e) {
const { sCheckedKeys, sHalfCheckedKeys, checkStrictly, $slots: { default: children }} = this
// Use map to optimize update speed
const checkedKeySet = {}
const halfCheckedKeySet = {}
sCheckedKeys.forEach(key => {
checkedKeySet[key] = true
})
sHalfCheckedKeys.forEach(key => {
halfCheckedKeySet[key] = true
})
// Batch process
this.checkedBatch.list.forEach(({ key, checked, halfChecked }) => {
checkedKeySet[key] = checked
halfCheckedKeySet[key] = halfChecked
})
const newCheckedKeys = Object.keys(checkedKeySet).filter(key => checkedKeySet[key])
const newHalfCheckedKeys = Object.keys(halfCheckedKeySet).filter(key => halfCheckedKeySet[key])
// Trigger onChecked
let selectedObj
// Prepare trigger arguments
let checkedObj
const eventObj = {
event: 'check',
node: this.checkedBatch.treeNode,
checked: this.checkedBatch.checked,
node: treeNode,
checked,
nativeEvent: e.nativeEvent,
}
if (checkStrictly) {
selectedObj = getStrictlyValue(newCheckedKeys, newHalfCheckedKeys)
const checkedKeys = checked ? arrAdd(oriCheckedKeys, eventKey) : arrDel(oriCheckedKeys, eventKey)
const halfCheckedKeys = arrDel(oriHalfCheckedKeys, eventKey)
checkedObj = { checked: checkedKeys, halfChecked: halfCheckedKeys }
// [Legacy] TODO: add optimize prop to skip node process
eventObj.checkedNodes = []
traverseTreeNodes(children, ({ node, key }) => {
if (checkedKeySet[key]) {
eventObj.checkedNodes.push(node)
}
eventObj.checkedNodes = checkedKeys
.map(key => keyEntities[key])
.filter(entity => entity)
.map(entity => entity.node)
this.setUncontrolledState({ _checkedKeys: checkedKeys })
} else {
const { checkedKeys, halfCheckedKeys } = conductCheck([eventKey], checked, keyEntities, {
checkedKeys: oriCheckedKeys, halfCheckedKeys: oriHalfCheckedKeys,
})
this.setUncontrolledState({ checkedKeys: newCheckedKeys })
} else {
selectedObj = newCheckedKeys
checkedObj = checkedKeys
// [Legacy] TODO: add optimize prop to skip node process
// [Legacy] This is used for `rc-tree-select`
eventObj.checkedNodes = []
eventObj.checkedNodesPositions = [] // [Legacy] TODO: not in API
eventObj.halfCheckedKeys = newHalfCheckedKeys // [Legacy] TODO: not in API
traverseTreeNodes(children, ({ node, pos, key }) => {
if (checkedKeySet[key]) {
eventObj.checkedNodes.push(node)
eventObj.checkedNodesPositions.push({ node, pos })
}
eventObj.checkedNodesPositions = []
eventObj.halfCheckedKeys = halfCheckedKeys
checkedKeys.forEach((key) => {
const entity = keyEntities[key]
if (!entity) return
const { node, pos } = entity
eventObj.checkedNodes.push(node)
eventObj.checkedNodesPositions.push({ node, pos })
})
this.setUncontrolledState({
checkedKeys: newCheckedKeys,
halfCheckedKeys: newHalfCheckedKeys,
_checkedKeys: checkedKeys,
_halfCheckedKeys: halfCheckedKeys,
})
}
this.__emit('check', selectedObj, eventObj)
this.__emit('check', checkedObj, eventObj)
},
onNodeLoad (treeNode) {
return new Promise((resolve) => {
// We need to get the latest state of loading/loaded keys
this.setState(({ _loadedKeys: loadedKeys = [], _loadingKeys: loadingKeys = [] }) => {
const { loadData } = this.$props
const { eventKey } = getOptionProps('treeNode')
// Clean up
this.checkedBatch = null
if (!loadData || loadedKeys.indexOf(eventKey) !== -1 || loadingKeys.indexOf(eventKey) !== -1) {
return {}
}
// Process load data
const promise = loadData(treeNode)
promise.then(() => {
const newLoadedKeys = arrAdd(this.$data._loadedKeys, eventKey)
this.setUncontrolledState({
_loadedKeys: newLoadedKeys,
})
this.setState({
_loadingKeys: arrDel(this.$data._loadingKeys, eventKey),
})
const eventObj = {
event: 'load',
node: treeNode,
}
this.__emit('load', eventObj)
resolve()
})
return {
loadingKeys: arrAdd(loadingKeys, eventKey),
}
})
})
},
onNodeExpand (e, treeNode) {
const { sExpandedKeys, loadData } = this
let expandedKeys = [...sExpandedKeys]
let { _expandedKeys: expandedKeys } = this.$data
const { loadData } = this.$props
const { eventKey, expanded } = getOptionProps(treeNode)
// Update selected keys
@ -490,7 +496,7 @@ const Tree = {
expandedKeys = arrDel(expandedKeys, eventKey)
}
this.setUncontrolledState({ expandedKeys })
this.setUncontrolledState({ _expandedKeys: expandedKeys })
this.__emit('expand', expandedKeys, {
node: treeNode,
expanded: targetExpanded,
@ -502,7 +508,7 @@ const Tree = {
const loadPromise = this.onNodeLoad(treeNode)
return loadPromise ? loadPromise.then(() => {
// [Legacy] Refresh logic
this.setUncontrolledState({ expandedKeys })
this.setUncontrolledState({ _expandedKeys: expandedKeys })
}) : null
}
@ -519,27 +525,7 @@ const Tree = {
onNodeContextMenu (event, node) {
event.preventDefault()
this.__emit('rightClick', { event, node })
},
/**
* Sync state with props if needed
*/
getSyncProps (props = {}) {
const newState = {}
const children = this.$slots.default
if (props.selectedKeys !== undefined) {
newState.sSelectedKeys = calcSelectedKeys(props.selectedKeys, props, children)
}
if (props.checkedKeys !== undefined) {
const { checkedKeys = [], halfCheckedKeys = [] } =
calcCheckedKeys(props.checkedKeys, props, children) || {}
newState.sCheckedKeys = checkedKeys
newState.sHalfCheckedKeys = halfCheckedKeys
}
return newState
this.__emit('rightclick', { event, node })
},
/**
@ -550,11 +536,9 @@ const Tree = {
const newState = {}
const props = getOptionProps(this)
Object.keys(state).forEach(name => {
if (name in props) return
if (name.replace('_', '') in props) return
needSync = true
const key = this.propsToStateMap[name]
newState[key] = state[name]
newState[name] = state[name]
})
if (needSync) {
@ -563,8 +547,8 @@ const Tree = {
},
isKeyChecked (key) {
const { sCheckedKeys = [] } = this
return sCheckedKeys.indexOf(key) !== -1
const { _checkedKeys: checkedKeys = [] } = this.$data
return checkedKeys.indexOf(key) !== -1
},
/**
@ -573,23 +557,31 @@ const Tree = {
*/
renderTreeNode (child, index, level = 0) {
const {
sExpandedKeys = [], sSelectedKeys = [], sHalfCheckedKeys = [],
sLoadedKeys = [], sLoadingKeys = [],
dragOverNodeKey, dropPosition,
} = this
_keyEntities: keyEntities,
_expandedKeys: expandedKeys = [],
_selectedKeys: selectedKeys = [],
_halfCheckedKeys: halfCheckedKeys = [],
_loadedKeys: loadedKeys = [], _loadingKeys: loadingKeys = [],
_dragOverNodeKey: dragOverNodeKey,
_dropPosition: dropPosition,
} = this.$data
const pos = getPosition(level, index)
const key = child.key || pos
if (!keyEntities[key]) {
warnOnlyTreeNode()
return null
}
return cloneElement(child, {
props: {
key,
eventKey: key,
expanded: sExpandedKeys.indexOf(key) !== -1,
selected: sSelectedKeys.indexOf(key) !== -1,
loaded: sLoadedKeys.indexOf(key) !== -1,
loading: sLoadingKeys.indexOf(key) !== -1,
expanded: expandedKeys.indexOf(key) !== -1,
selected: selectedKeys.indexOf(key) !== -1,
loaded: loadedKeys.indexOf(key) !== -1,
loading: loadingKeys.indexOf(key) !== -1,
checked: this.isKeyChecked(key),
halfChecked: sHalfCheckedKeys.indexOf(key) !== -1,
halfChecked: halfCheckedKeys.indexOf(key) !== -1,
pos,
// [Legacy] Drag props
@ -597,17 +589,16 @@ const Tree = {
dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
},
})
},
},
render () {
const { _treeNode: treeNode } = this.$data
const {
prefixCls, focusable,
showLine,
$slots: { default: children = [] },
} = this
showLine, tabIndex = 0,
} = this.$props
const domProps = {}
return (
@ -618,10 +609,10 @@ const Tree = {
})}
role='tree-node'
unselectable='on'
tabIndex={focusable ? '0' : null}
tabIndex={focusable ? tabIndex : null}
onKeydown={focusable ? this.onKeydown : () => {}}
>
{mapChildren(children, (node, index) => (
{mapChildren(treeNode, (node, index) => (
this.renderTreeNode(node, index)
))}
</ul>
@ -629,4 +620,6 @@ const Tree = {
},
}
export default Tree
export { Tree }
export default proxyComponent(Tree)

View File

@ -1,10 +1,12 @@
import PropTypes from '../../_util/vue-types'
import classNames from 'classnames'
import warning from 'warning'
import { getPosition, getNodeChildren, isCheckDisabled, traverseTreeNodes, mapChildren } from './util'
import { initDefaultProps, getOptionProps, filterEmpty, getComponentFromProp } from '../../_util/props-util'
import { getNodeChildren,
mapChildren,
warnOnlyTreeNode } from './util'
import { initDefaultProps, filterEmpty, getComponentFromProp } from '../../_util/props-util'
import BaseMixin from '../../_util/BaseMixin'
import getTransitionProps from '../../_util/getTransitionProps'
import { cloneElement } from '../../_util/vnode'
function noop () {}
const ICON_OPEN = 'open'
@ -12,8 +14,6 @@ const ICON_CLOSE = 'close'
const defaultTitle = '---'
let onlyTreeNodeWarned = false // Only accept TreeNode
const TreeNode = {
name: 'TreeNode',
mixins: [BaseMixin],
@ -45,6 +45,7 @@ const TreeNode = {
disableCheckbox: PropTypes.bool,
icon: PropTypes.any,
dataRef: PropTypes.object,
switcherIcon: PropTypes.any,
}, {}),
data () {
@ -66,84 +67,11 @@ const TreeNode = {
mounted () {
this.syncLoadData(this.$props)
},
watch: {
expanded (val) {
this.syncLoadData({ expanded: val })
},
updated () {
this.syncLoadData(this.$props)
},
methods: {
onUpCheckConduct (treeNode, nodeChecked, nodeHalfChecked, e) {
const { pos: nodePos } = getOptionProps(treeNode)
const { eventKey, pos, checked, halfChecked } = this
const {
vcTree: { checkStrictly, isKeyChecked, onBatchNodeCheck, onCheckConductFinished },
vcTreeNode: { onUpCheckConduct } = {},
} = this
// Stop conduct when current node is disabled
if (isCheckDisabled(this)) {
onCheckConductFinished(e)
return
}
const children = this.getNodeChildren()
let checkedCount = nodeChecked ? 1 : 0
// Statistic checked count
children.forEach((node, index) => {
const childPos = getPosition(pos, index)
if (nodePos === childPos || isCheckDisabled(node)) {
return
}
if (isKeyChecked(node.key || childPos)) {
checkedCount += 1
}
})
// Static enabled children count
const enabledChildrenCount = children
.filter(node => !isCheckDisabled(node))
.length
// checkStrictly will not conduct check status
const nextChecked = checkStrictly ? checked : enabledChildrenCount === checkedCount
const nextHalfChecked = checkStrictly // propagated or child checked
? halfChecked : (nodeHalfChecked || (checkedCount > 0 && !nextChecked))
// Add into batch update
if (checked !== nextChecked || halfChecked !== nextHalfChecked) {
onBatchNodeCheck(eventKey, nextChecked, nextHalfChecked)
if (onUpCheckConduct) {
onUpCheckConduct(this, nextChecked, nextHalfChecked, e)
} else {
// Flush all the update
onCheckConductFinished(e)
}
} else {
// Flush all the update
onCheckConductFinished(e)
}
},
onDownCheckConduct (nodeChecked) {
const { $slots } = this
const children = $slots.default || []
const { vcTree: { checkStrictly, isKeyChecked, onBatchNodeCheck }} = this
if (checkStrictly) return
traverseTreeNodes(children, ({ node, key }) => {
if (isCheckDisabled(node)) return false
if (nodeChecked !== isKeyChecked(key)) {
onBatchNodeCheck(key, nodeChecked, false)
}
})
},
onSelectorClick (e) {
// Click trigger before select/check operation
const { vcTree: { onNodeClick }} = this
@ -171,27 +99,16 @@ const TreeNode = {
onCheck (e) {
if (this.isDisabled()) return
const { disableCheckbox, checked, eventKey } = this
const { disableCheckbox, checked } = this
const {
vcTree: { checkable, onBatchNodeCheck, onCheckConductFinished },
vcTreeNode: { onUpCheckConduct } = {},
vcTree: { checkable, onNodeCheck },
} = this
if (!checkable || disableCheckbox) return
e.preventDefault()
const targetChecked = !checked
onBatchNodeCheck(eventKey, targetChecked, false, this)
// Children conduct
this.onDownCheckConduct(targetChecked)
// Parent conduct
if (onUpCheckConduct) {
onUpCheckConduct(this, targetChecked, false, e)
} else {
onCheckConductFinished(e)
}
onNodeCheck(e, this, targetChecked)
},
onMouseEnter (e) {
@ -282,9 +199,8 @@ const TreeNode = {
const originList = filterEmpty(children)
const targetList = getNodeChildren(originList)
if (originList.length !== targetList.length && !onlyTreeNodeWarned) {
onlyTreeNodeWarned = true
warning(false, 'Tree only accept TreeNode as children.')
if (originList.length !== targetList.length) {
warnOnlyTreeNode()
}
return targetList
@ -341,15 +257,15 @@ const TreeNode = {
// Load data to avoid default expanded tree without data
syncLoadData (props) {
const { expanded } = this
const { expanded, loading, loaded } = props
const { vcTree: { onNodeLoad }} = this
if (loading) return
// read from state to avoid loadData at same time
if (expanded && !this.isLeaf2()) {
// We needn't reload data when has children in sync logic
// It's only needed in node expanded
const hasChildren = this.getNodeChildren().length !== 0
if (!hasChildren) {
if (!hasChildren && !loaded) {
onNodeLoad(this)
}
}
@ -359,19 +275,22 @@ const TreeNode = {
renderSwitcher () {
const { expanded } = this
const { vcTree: { prefixCls }} = this
const switcherIcon = getComponentFromProp(this, 'switcherIcon') || getComponentFromProp(this.vcTree, 'switcherIcon')
if (this.isLeaf2()) {
return <span class={`${prefixCls}-switcher ${prefixCls}-switcher-noop`} />
return (
<span key='switcher' class={classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher-noop`)}>
{typeof switcherIcon === 'function'
? cloneElement(switcherIcon, { props: { ...this.$props, isLeaf: true }}) : switcherIcon}
</span>
)
}
const switcherCls = classNames(`${prefixCls}-switcher`, `${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`)
return (
<span
class={classNames(
`${prefixCls}-switcher`,
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
)}
onClick={this.onExpand}
/>
<span key='switcher' onClick={this.onExpand} class={switcherCls}>
{typeof switcherIcon === 'function'
? cloneElement(switcherIcon, { props: { ...this.$props, isLeaf: false }}) : switcherIcon}
</span>
)
},
@ -388,6 +307,7 @@ const TreeNode = {
return (
<span
key='checkbox'
class={classNames(
`${prefixCls}-checkbox`,
checked && `${prefixCls}-checkbox-checked`,
@ -407,6 +327,7 @@ const TreeNode = {
return (
<span
key='icon'
class={classNames(
`${prefixCls}-iconEle`,
`${prefixCls}-icon__${this.getNodeState() || 'docu'}`,
@ -450,6 +371,7 @@ const TreeNode = {
return (
<span
key='selector'
ref='selectHandle'
title={typeof title === 'string' ? title : ''}
class={classNames(
@ -481,23 +403,12 @@ const TreeNode = {
renderTreeNode,
}} = this
// [Legacy] Animation control
const renderFirst = this.renderFirst
this.renderFirst = 1
let transitionAppear = true
if (!renderFirst && expanded) {
transitionAppear = false
}
let animProps = {}
if (openTransitionName) {
animProps = getTransitionProps(openTransitionName, { appear: transitionAppear })
animProps = getTransitionProps(openTransitionName)
} else if (typeof openAnimation === 'object') {
animProps = { ...openAnimation }
animProps.props = { css: false, ...animProps.props }
if (!transitionAppear) {
delete animProps.props.appear
}
}
// Children TreeNode
@ -516,6 +427,7 @@ const TreeNode = {
expanded && `${prefixCls}-child-tree-open`,
)}
data-expanded={expanded}
role='group'
>
{mapChildren(nodeList, (node, index) => (
renderTreeNode(node, index, pos)
@ -560,6 +472,7 @@ const TreeNode = {
'drag-over-gap-bottom': !disabled && dragOverGapBottom,
'filter-node': filterTreeNode && filterTreeNode(this),
}}
role='treeitem'
onDragenter={draggable ? this.onDragEnter : noop}
onDragover={draggable ? this.onDragOver : noop}
onDragleave={draggable ? this.onDragLeave : noop}

View File

@ -1,25 +1,7 @@
import { getOptionProps } from '../../_util/props-util'
import Tree from './Tree'
import ProxyTree, { Tree } from './Tree'
import TreeNode from './TreeNode'
Tree.TreeNode = TreeNode
ProxyTree.TreeNode = TreeNode
//
const NewTree = {
TreeNode: TreeNode,
props: Tree.props,
render () {
const { $listeners, $slots } = this
const treeProps = {
props: {
...getOptionProps(this),
children: $slots.default,
},
on: $listeners,
}
return (
<Tree {...treeProps}>{$slots.default}</Tree>
)
},
}
export { TreeNode }
export default NewTree
export { Tree, TreeNode }
export default ProxyTree

View File

@ -0,0 +1,378 @@
/* eslint no-loop-func: 0*/
import warning from 'warning'
import { getSlotOptions, getOptionProps } from '../../_util/props-util'
const DRAG_SIDE_RANGE = 0.25
const DRAG_MIN_GAP = 2
let onlyTreeNodeWarned = false
export function warnOnlyTreeNode () {
if (onlyTreeNodeWarned) return
onlyTreeNodeWarned = true
warning(false, 'Tree only accept TreeNode as children.')
}
export function arrDel (list, value) {
const clone = list.slice()
const index = clone.indexOf(value)
if (index >= 0) {
clone.splice(index, 1)
}
return clone
}
export function arrAdd (list, value) {
const clone = list.slice()
if (clone.indexOf(value) === -1) {
clone.push(value)
}
return clone
}
export function posToArr (pos) {
return pos.split('-')
}
export function getPosition (level, index) {
return `${level}-${index}`
}
export function isTreeNode (node) {
return getSlotOptions(node).isTreeNode
}
export function getNodeChildren (children = []) {
return children.filter(isTreeNode)
}
export function isCheckDisabled (node) {
const { disabled, disableCheckbox } = getOptionProps(node) || {}
return !!(disabled || disableCheckbox)
}
export function traverseTreeNodes (treeNodes, subTreeData, callback) {
function processNode (node, index, parent) {
const children = node ? node.componentOptions.children : treeNodes
const pos = node ? getPosition(parent.pos, index) : 0
// Filter children
const childList = getNodeChildren(children)
// Process node if is not root
if (node) {
const data = {
node,
index,
pos,
key: node.key || pos,
parentPos: parent.node ? parent.pos : null,
}
callback(data)
}
// Process children node
childList.forEach((subNode, subIndex) => {
processNode(subNode, subIndex, { node, pos })
})
}
processNode(null)
}
/**
* Use `rc-util` `toArray` to get the children list which keeps the key.
* And return single node if children is only one(This can avoid `key` missing check).
*/
export function mapChildren (children = [], func) {
const list = children.map(func)
if (list.length === 1) {
return list[0]
}
return list
}
/**
* [Legacy] Return halfChecked when it has value.
* @param checkedKeys
* @param halfChecked
* @returns {*}
*/
export function getStrictlyValue (checkedKeys, halfChecked) {
if (halfChecked) {
return { checked: checkedKeys, halfChecked }
}
return checkedKeys
}
export function getFullKeyList (treeNodes) {
const keyList = []
traverseTreeNodes(treeNodes, ({ key }) => {
keyList.push(key)
})
return keyList
}
/**
* Check position relation.
* @param parentPos
* @param childPos
* @param directly only directly parent can be true
* @returns {boolean}
*/
export function isParent (parentPos, childPos, directly = false) {
if (!parentPos || !childPos || parentPos.length > childPos.length) return false
const parentPath = posToArr(parentPos)
const childPath = posToArr(childPos)
// Directly check
if (directly && parentPath.length !== childPath.length - 1) return false
const len = parentPath.length
for (let i = 0; i < len; i += 1) {
if (parentPath[i] !== childPath[i]) return false
}
return true
}
/**
* Statistic TreeNodes info
* @param treeNodes
* @returns {{}}
*/
export function getNodesStatistic (treeNodes) {
const statistic = {
keyNodes: {},
posNodes: {},
nodeList: [],
}
traverseTreeNodes(treeNodes, true, ({ node, index, pos, key, subNodes, parentPos }) => {
const data = { node, index, pos, key, subNodes, parentPos }
statistic.keyNodes[key] = data
statistic.posNodes[pos] = data
statistic.nodeList.push(data)
})
return statistic
}
export function getDragNodesKeys (treeNodes, node) {
const { eventKey, pos } = getOptionProps(node)
const dragNodesKeys = []
traverseTreeNodes(treeNodes, ({ pos: nodePos, key }) => {
if (isParent(pos, nodePos)) {
dragNodesKeys.push(key)
}
})
dragNodesKeys.push(eventKey || pos)
return dragNodesKeys
}
export function calcDropPosition (event, treeNode) {
const { clientY } = event
const { top, bottom, height } = treeNode.$refs.selectHandle.getBoundingClientRect()
const des = Math.max(height * DRAG_SIDE_RANGE, DRAG_MIN_GAP)
if (clientY <= top + des) {
return -1
} else if (clientY >= bottom - des) {
return 1
}
return 0
}
/**
* Auto expand all related node when sub node is expanded
* @param keyList
* @param props
* @returns [string]
*/
export function calcExpandedKeys (keyList, props, children = []) {
if (!keyList) {
return []
}
// Fill parent expanded keys
const { keyNodes, nodeList } = getNodesStatistic(children)
const needExpandKeys = {}
const needExpandPathList = []
// Fill expanded nodes
keyList.forEach((key) => {
const node = keyNodes[key]
if (node) {
needExpandKeys[key] = true
needExpandPathList.push(node.pos)
}
})
// Match parent by path
nodeList.forEach(({ pos, key }) => {
if (needExpandPathList.some(childPos => isParent(pos, childPos))) {
needExpandKeys[key] = true
}
})
const calcExpandedKeyList = Object.keys(needExpandKeys)
// [Legacy] Return origin keyList if calc list is empty
return calcExpandedKeyList.length ? calcExpandedKeyList : keyList
}
/**
* Return selectedKeys according with multiple prop
* @param selectedKeys
* @param props
* @returns [string]
*/
export function calcSelectedKeys (selectedKeys, props) {
if (!selectedKeys) {
return undefined
}
const { multiple } = props
if (multiple) {
return selectedKeys.slice()
}
if (selectedKeys.length) {
return [selectedKeys[0]]
}
return selectedKeys
}
/**
* Check conduct is by key level. It pass though up & down.
* When conduct target node is check means already conducted will be skip.
* @param treeNodes
* @param checkedKeys
* @returns {{checkedKeys: Array, halfCheckedKeys: Array}}
*/
export function calcCheckStateConduct (treeNodes, checkedKeys) {
const { keyNodes, posNodes } = getNodesStatistic(treeNodes)
const tgtCheckedKeys = {}
const tgtHalfCheckedKeys = {}
// Conduct up
function conductUp (key, halfChecked) {
if (tgtCheckedKeys[key]) return
const { subNodes = [], parentPos, node } = keyNodes[key]
if (isCheckDisabled(node)) return
const allSubChecked = !halfChecked && subNodes
.filter(sub => !isCheckDisabled(sub.node))
.every(sub => tgtCheckedKeys[sub.key])
if (allSubChecked) {
tgtCheckedKeys[key] = true
} else {
tgtHalfCheckedKeys[key] = true
}
if (parentPos !== null) {
conductUp(posNodes[parentPos].key, !allSubChecked)
}
}
// Conduct down
function conductDown (key) {
if (tgtCheckedKeys[key]) return
const { subNodes = [], node } = keyNodes[key]
if (isCheckDisabled(node)) return
tgtCheckedKeys[key] = true
subNodes.forEach((sub) => {
conductDown(sub.key)
})
}
function conduct (key) {
if (!keyNodes[key]) {
warning(false, `'${key}' does not exist in the tree.`)
return
}
const { subNodes = [], parentPos, node } = keyNodes[key]
tgtCheckedKeys[key] = true
if (isCheckDisabled(node)) return
// Conduct down
subNodes
.filter(sub => !isCheckDisabled(sub.node))
.forEach((sub) => {
conductDown(sub.key)
})
// Conduct up
if (parentPos !== null) {
conductUp(posNodes[parentPos].key)
}
}
checkedKeys.forEach((key) => {
conduct(key)
})
return {
checkedKeys: Object.keys(tgtCheckedKeys),
halfCheckedKeys: Object.keys(tgtHalfCheckedKeys)
.filter(key => !tgtCheckedKeys[key]),
}
}
function keyListToString (keyList) {
if (!keyList) return keyList
return keyList.map(key => String(key))
}
/**
* Calculate the value of checked and halfChecked keys.
* This should be only run in init or props changed.
*/
export function calcCheckedKeys (keys, props, children = []) {
const { checkable, checkStrictly } = props
if (!checkable || !keys) {
return null
}
// Convert keys to object format
let keyProps
if (Array.isArray(keys)) {
// [Legacy] Follow the api doc
keyProps = {
checkedKeys: keys,
halfCheckedKeys: undefined,
}
} else if (typeof keys === 'object') {
keyProps = {
checkedKeys: keys.checked || undefined,
halfCheckedKeys: keys.halfChecked || undefined,
}
} else {
warning(false, '`CheckedKeys` is not an array or an object')
return null
}
keyProps.checkedKeys = keyListToString(keyProps.checkedKeys)
keyProps.halfCheckedKeys = keyListToString(keyProps.halfCheckedKeys)
// Do nothing if is checkStrictly mode
if (checkStrictly) {
return keyProps
}
// Conduct calculate the check status
const { checkedKeys = [] } = keyProps
return calcCheckStateConduct(children, checkedKeys)
}

View File

@ -1,9 +1,20 @@
/* eslint no-loop-func: 0*/
import warning from 'warning'
import omit from 'omit.js'
import TreeNode from './TreeNode'
import { getSlotOptions, getOptionProps } from '../../_util/props-util'
const DRAG_SIDE_RANGE = 0.25
const DRAG_MIN_GAP = 2
let onlyTreeNodeWarned = false
export function warnOnlyTreeNode () {
if (onlyTreeNodeWarned) return
onlyTreeNodeWarned = true
warning(false, 'Tree only accept TreeNode as children.')
}
export function arrDel (list, value) {
const clone = list.slice()
const index = clone.indexOf(value)
@ -29,9 +40,12 @@ export function getPosition (level, index) {
return `${level}-${index}`
}
export function isTreeNode (node) {
return getSlotOptions(node).isTreeNode
}
export function getNodeChildren (children = []) {
return children
.filter(child => getSlotOptions(child).isTreeNode)
return children.filter(isTreeNode)
}
export function isCheckDisabled (node) {
@ -39,12 +53,7 @@ export function isCheckDisabled (node) {
return !!(disabled || disableCheckbox)
}
export function traverseTreeNodes (treeNodes, subTreeData, callback) {
if (typeof subTreeData === 'function') {
callback = subTreeData
subTreeData = false
}
export function traverseTreeNodes (treeNodes, callback) {
function processNode (node, index, parent) {
const children = node ? node.componentOptions.children : treeNodes
const pos = node ? getPosition(parent.pos, index) : 0
@ -61,28 +70,7 @@ export function traverseTreeNodes (treeNodes, subTreeData, callback) {
key: node.key || pos,
parentPos: parent.node ? parent.pos : null,
}
// Children data is not must have
if (subTreeData) {
// Statistic children
const subNodes = []
childList.forEach((subNode, subIndex) => {
// Provide limit snapshot
const subPos = getPosition(pos, index)
subNodes.push({
node: subNode,
key: subNode.key || subPos,
pos: subPos,
index: subIndex,
})
})
data.subNodes = subNodes
}
// Can break traverse by return false
if (callback(data) === false) {
return
}
callback(data)
}
// Process children node
@ -106,81 +94,12 @@ export function mapChildren (children = [], func) {
return list
}
/**
* [Legacy] Return halfChecked when it has value.
* @param checkedKeys
* @param halfChecked
* @returns {*}
*/
export function getStrictlyValue (checkedKeys, halfChecked) {
if (halfChecked) {
return { checked: checkedKeys, halfChecked }
}
return checkedKeys
}
export function getFullKeyList (treeNodes) {
const keyList = []
traverseTreeNodes(treeNodes, ({ key }) => {
keyList.push(key)
})
return keyList
}
/**
* Check position relation.
* @param parentPos
* @param childPos
* @param directly only directly parent can be true
* @returns {boolean}
*/
export function isParent (parentPos, childPos, directly = false) {
if (!parentPos || !childPos || parentPos.length > childPos.length) return false
const parentPath = posToArr(parentPos)
const childPath = posToArr(childPos)
// Directly check
if (directly && parentPath.length !== childPath.length - 1) return false
const len = parentPath.length
for (let i = 0; i < len; i += 1) {
if (parentPath[i] !== childPath[i]) return false
}
return true
}
/**
* Statistic TreeNodes info
* @param treeNodes
* @returns {{}}
*/
export function getNodesStatistic (treeNodes) {
const statistic = {
keyNodes: {},
posNodes: {},
nodeList: [],
}
traverseTreeNodes(treeNodes, true, ({ node, index, pos, key, subNodes, parentPos }) => {
const data = { node, index, pos, key, subNodes, parentPos }
statistic.keyNodes[key] = data
statistic.posNodes[pos] = data
statistic.nodeList.push(data)
})
return statistic
}
export function getDragNodesKeys (treeNodes, node) {
const { eventKey, pos } = getOptionProps(node)
const dragNodesKeys = []
traverseTreeNodes(treeNodes, ({ pos: nodePos, key }) => {
if (isParent(pos, nodePos)) {
dragNodesKeys.push(key)
}
traverseTreeNodes(treeNodes, ({ key }) => {
dragNodesKeys.push(key)
})
dragNodesKeys.push(eventKey || pos)
return dragNodesKeys
@ -199,44 +118,6 @@ export function calcDropPosition (event, treeNode) {
return 0
}
/**
* Auto expand all related node when sub node is expanded
* @param keyList
* @param props
* @returns [string]
*/
export function calcExpandedKeys (keyList, props, children = []) {
if (!keyList) {
return []
}
// Fill parent expanded keys
const { keyNodes, nodeList } = getNodesStatistic(children)
const needExpandKeys = {}
const needExpandPathList = []
// Fill expanded nodes
keyList.forEach((key) => {
const node = keyNodes[key]
if (node) {
needExpandKeys[key] = true
needExpandPathList.push(node.pos)
}
})
// Match parent by path
nodeList.forEach(({ pos, key }) => {
if (needExpandPathList.some(childPos => isParent(pos, childPos))) {
needExpandKeys[key] = true
}
})
const calcExpandedKeyList = Object.keys(needExpandKeys)
// [Legacy] Return origin keyList if calc list is empty
return calcExpandedKeyList.length ? calcExpandedKeyList : keyList
}
/**
* Return selectedKeys according with multiple prop
* @param selectedKeys
@ -244,9 +125,7 @@ export function calcExpandedKeys (keyList, props, children = []) {
* @returns [string]
*/
export function calcSelectedKeys (selectedKeys, props) {
if (!selectedKeys) {
return undefined
}
if (!selectedKeys) { return undefined }
const { multiple } = props
if (multiple) {
@ -260,103 +139,87 @@ export function calcSelectedKeys (selectedKeys, props) {
}
/**
* Check conduct is by key level. It pass though up & down.
* When conduct target node is check means already conducted will be skip.
* @param treeNodes
* @param checkedKeys
* @returns {{checkedKeys: Array, halfCheckedKeys: Array}}
* Since React internal will convert key to string,
* we need do this to avoid `checkStrictly` use number match
*/
export function calcCheckStateConduct (treeNodes, checkedKeys) {
const { keyNodes, posNodes } = getNodesStatistic(treeNodes)
const tgtCheckedKeys = {}
const tgtHalfCheckedKeys = {}
// Conduct up
function conductUp (key, halfChecked) {
if (tgtCheckedKeys[key]) return
const { subNodes = [], parentPos, node } = keyNodes[key]
if (isCheckDisabled(node)) return
const allSubChecked = !halfChecked && subNodes
.filter(sub => !isCheckDisabled(sub.node))
.every(sub => tgtCheckedKeys[sub.key])
if (allSubChecked) {
tgtCheckedKeys[key] = true
} else {
tgtHalfCheckedKeys[key] = true
}
if (parentPos !== null) {
conductUp(posNodes[parentPos].key, !allSubChecked)
}
}
// Conduct down
function conductDown (key) {
if (tgtCheckedKeys[key]) return
const { subNodes = [], node } = keyNodes[key]
if (isCheckDisabled(node)) return
tgtCheckedKeys[key] = true
subNodes.forEach((sub) => {
conductDown(sub.key)
})
}
function conduct (key) {
if (!keyNodes[key]) {
warning(false, `'${key}' does not exist in the tree.`)
return
}
const { subNodes = [], parentPos, node } = keyNodes[key]
tgtCheckedKeys[key] = true
if (isCheckDisabled(node)) return
// Conduct down
subNodes
.filter(sub => !isCheckDisabled(sub.node))
.forEach((sub) => {
conductDown(sub.key)
})
// Conduct up
if (parentPos !== null) {
conductUp(posNodes[parentPos].key)
}
}
checkedKeys.forEach((key) => {
conduct(key)
})
return {
checkedKeys: Object.keys(tgtCheckedKeys),
halfCheckedKeys: Object.keys(tgtHalfCheckedKeys)
.filter(key => !tgtCheckedKeys[key]),
}
}
function keyListToString (keyList) {
if (!keyList) return keyList
return keyList.map(key => String(key))
}
/**
* Calculate the value of checked and halfChecked keys.
* This should be only run in init or props changed.
*/
export function calcCheckedKeys (keys, props, children = []) {
const { checkable, checkStrictly } = props
const internalProcessProps = (props = {}) => {
return {
props: omit(props, ['on', 'key', 'class', 'className', 'style']),
on: props.on || {},
class: props.class || props.className,
style: props.style,
key: props.key,
}
}
export function convertDataToTree (h, treeData, processer) {
if (!treeData) return []
if (!checkable || !keys) {
const { processProps = internalProcessProps } = processer || {}
const list = Array.isArray(treeData) ? treeData : [treeData]
return list.map(({ children, ...props }) => {
const childrenNodes = convertDataToTree(h, children, processer)
return (
<TreeNode {...processProps(props) }>
{childrenNodes}
</TreeNode>
)
})
}
// TODO: ========================= NEW LOGIC =========================
/**
* Calculate treeNodes entities. `processTreeEntity` is used for `rc-tree-select`
* @param treeNodes
* @param processTreeEntity User can customize the entity
*/
export function convertTreeToEntities (treeNodes, { initWrapper, processEntity, onProcessFinished } = {}) {
const posEntities = {}
const keyEntities = {}
let wrapper = {
posEntities,
keyEntities,
}
if (initWrapper) {
wrapper = initWrapper(wrapper) || wrapper
}
traverseTreeNodes(treeNodes, (item) => {
const { node, index, pos, key, parentPos } = item
const entity = { node, index, key, pos }
posEntities[pos] = entity
keyEntities[key] = entity
// Fill children
entity.parent = posEntities[parentPos]
if (entity.parent) {
entity.parent.children = entity.parent.children || []
entity.parent.children.push(entity)
}
if (processEntity) {
processEntity(entity, wrapper)
}
})
if (onProcessFinished) {
onProcessFinished(wrapper)
}
return wrapper
}
/**
* Parse `checkedKeys` to { checkedKeys, halfCheckedKeys } style
*/
export function parseCheckedKeys (keys) {
if (!keys) {
return null
}
@ -374,19 +237,186 @@ export function calcCheckedKeys (keys, props, children = []) {
halfCheckedKeys: keys.halfChecked || undefined,
}
} else {
warning(false, '`CheckedKeys` is not an array or an object')
warning(false, '`checkedKeys` is not an array or an object')
return null
}
keyProps.checkedKeys = keyListToString(keyProps.checkedKeys)
keyProps.halfCheckedKeys = keyListToString(keyProps.halfCheckedKeys)
// Do nothing if is checkStrictly mode
if (checkStrictly) {
return keyProps
return keyProps
}
/**
* Conduct check state by the keyList. It will conduct up & from the provided key.
* If the conduct path reach the disabled or already checked / unchecked node will stop conduct.
* @param keyList list of keys
* @param isCheck is check the node or not
* @param keyEntities parsed by `convertTreeToEntities` function in Tree
* @param checkStatus Can pass current checked status for process (usually for uncheck operation)
* @returns {{checkedKeys: [], halfCheckedKeys: []}}
*/
export function conductCheck (keyList, isCheck, keyEntities, checkStatus = {}) {
const checkedKeys = {}
const halfCheckedKeys = {}; // Record the key has some child checked (include child half checked)
(checkStatus.checkedKeys || []).forEach((key) => {
checkedKeys[key] = true
});
(checkStatus.halfCheckedKeys || []).forEach((key) => {
halfCheckedKeys[key] = true
})
// Conduct up
function conductUp (key) {
if (checkedKeys[key] === isCheck) return
const entity = keyEntities[key]
if (!entity) return
const { children, parent, node } = entity
if (isCheckDisabled(node)) return
// Check child node checked status
let everyChildChecked = true
let someChildChecked = false; // Child checked or half checked
(children || [])
.filter(child => !isCheckDisabled(child.node))
.forEach(({ key: childKey }) => {
const childChecked = checkedKeys[childKey]
const childHalfChecked = halfCheckedKeys[childKey]
if (childChecked || childHalfChecked) someChildChecked = true
if (!childChecked) everyChildChecked = false
})
// Update checked status
if (isCheck) {
checkedKeys[key] = everyChildChecked
} else {
checkedKeys[key] = false
}
halfCheckedKeys[key] = someChildChecked
if (parent) {
conductUp(parent.key)
}
}
// Conduct calculate the check status
const { checkedKeys = [] } = keyProps
return calcCheckStateConduct(children, checkedKeys)
// Conduct down
function conductDown (key) {
if (checkedKeys[key] === isCheck) return
const entity = keyEntities[key]
if (!entity) return
const { children, node } = entity
if (isCheckDisabled(node)) return
checkedKeys[key] = isCheck;
(children || []).forEach((child) => {
conductDown(child.key)
})
}
function conduct (key) {
const entity = keyEntities[key]
if (!entity) {
warning(false, `'${key}' does not exist in the tree.`)
return
}
const { children, parent, node } = entity
checkedKeys[key] = isCheck
if (isCheckDisabled(node)) return;
// Conduct down
(children || [])
.filter(child => !isCheckDisabled(child.node))
.forEach((child) => {
conductDown(child.key)
})
// Conduct up
if (parent) {
conductUp(parent.key)
}
}
(keyList || []).forEach((key) => {
conduct(key)
})
const checkedKeyList = []
const halfCheckedKeyList = []
// Fill checked list
Object.keys(checkedKeys).forEach((key) => {
if (checkedKeys[key]) {
checkedKeyList.push(key)
}
})
// Fill half checked list
Object.keys(halfCheckedKeys).forEach((key) => {
if (!checkedKeys[key] && halfCheckedKeys[key]) {
halfCheckedKeyList.push(key)
}
})
return {
checkedKeys: checkedKeyList,
halfCheckedKeys: halfCheckedKeyList,
}
}
/**
* If user use `autoExpandParent` we should get the list of parent node
* @param keyList
* @param keyEntities
*/
export function conductExpandParent (keyList, keyEntities) {
const expandedKeys = {}
function conductUp (key) {
if (expandedKeys[key]) return
const entity = keyEntities[key]
if (!entity) return
expandedKeys[key] = true
const { parent, node } = entity
if (isCheckDisabled(node)) return
if (parent) {
conductUp(parent.key)
}
}
(keyList || []).forEach((key) => {
conductUp(key)
})
return Object.keys(expandedKeys)
}
/**
* Returns only the data- and aria- key/value pairs
* @param {object} props
*/
export function getDataAndAria (props) {
return Object.keys(props).reduce((prev, key) => {
if ((key.substr(0, 5) === 'data-' || key.substr(0, 5) === 'aria-')) {
prev[key] = props[key]
}
return prev
}, {})
}