tangjinzhou
6 years ago
14 changed files with 200 additions and 704 deletions
@ -0,0 +1,38 @@
|
||||
import { mount } from '@vue/test-utils' |
||||
import Tree from '../index' |
||||
import { calcRangeKeys } from '../util' |
||||
|
||||
const TreeNode = Tree.TreeNode |
||||
|
||||
describe('Tree util', () => { |
||||
it('calc range keys', () => { |
||||
const wrapper = mount({ |
||||
render () { |
||||
return ( |
||||
<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 treeWrapper = wrapper.find({ name: 'ATree' }) |
||||
const keys = calcRangeKeys(treeWrapper.vm.$slots.default, ['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()) |
||||
}) |
||||
}) |
@ -0,0 +1,43 @@
|
||||
<cn> |
||||
#### 目录 |
||||
内置的目录树,`multiple` 模式支持 `ctrl(Windows)` / `command(Mac)` 复选。 |
||||
</cn> |
||||
|
||||
<us> |
||||
#### Directory |
||||
Built-in directory tree. `multiple` support `ctrl(Windows)` / `command(Mac)` selection. |
||||
</us> |
||||
|
||||
```html |
||||
<template> |
||||
<a-directory-tree |
||||
multiple |
||||
defaultExpandAll |
||||
@select="onSelect" |
||||
@expand="onExpand" |
||||
> |
||||
<a-tree-node title="parent 0" key="0-0"> |
||||
<a-tree-node title="leaf 0-0" key="0-0-0" isLeaf /> |
||||
<a-tree-node title="leaf 0-1" key="0-0-1" isLeaf /> |
||||
</a-tree-node> |
||||
<a-tree-node title="parent 1" key="0-1"> |
||||
<a-tree-node title="leaf 1-0" key="0-1-0" isLeaf /> |
||||
<a-tree-node title="leaf 1-1" key="0-1-1" isLeaf /> |
||||
</a-tree-node> |
||||
</a-directory-tree> |
||||
</template> |
||||
<script> |
||||
export default { |
||||
methods: { |
||||
onSelect (keys) { |
||||
console.log('Trigger Select', keys); |
||||
}, |
||||
onExpand () { |
||||
console.log('Trigger Expand'); |
||||
}, |
||||
}, |
||||
} |
||||
|
||||
</script> |
||||
|
||||
``` |
@ -1,183 +0,0 @@
|
||||
|
||||
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 |
@ -1,378 +0,0 @@
|
||||
/* 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) |
||||
} |
Loading…
Reference in new issue