add vc-tree

This commit is contained in:
tjz 2018-04-11 21:25:16 +08:00
parent 388adbac35
commit 8e6dd94107
8 changed files with 516 additions and 75 deletions

View File

@ -0,0 +1,72 @@
<script>
/* eslint no-console:0 */
import Tree, { TreeNode } from '../index'
import '../assets/index.less'
import cssAnimation from 'css-animation'
function animate (node, show, done) {
let height = node.offsetHeight
return cssAnimation(node, 'collapse', {
start () {
if (!show) {
node.style.height = `${node.offsetHeight}px`
} else {
height = node.offsetHeight
node.style.height = 0
}
},
active () {
node.style.height = `${show ? height : 0}px`
},
end () {
node.style.height = ''
done()
},
})
}
const animation = {
enter (node, done) {
return animate(node, true, done)
},
leave (node, done) {
return animate(node, false, done)
},
}
export default {
render () {
return (
<div>
<h2>expanded</h2>
<Tree
defaultExpandAll={false}
defaultExpandedKeys={['p1']}
openAnimation={{ on: animation, props: { appear: true }}}
>
<TreeNode title='parent 1' key='p1'>
<TreeNode key='p10' title='leaf'/>
<TreeNode title='parent 1-1' key='p11'>
<TreeNode title='parent 2-1' key='p21'>
<TreeNode title='leaf'/>
<TreeNode title='leaf'/>
</TreeNode>
<TreeNode key='p22' title='leaf'/>
</TreeNode>
</TreeNode>
</Tree>
</div>
)
},
}
</script>
<style>
.collapse {
overflow: hidden;
display: block;
}
.collapse-active {
transition: height 0.3s ease-out;
}
</style>

View File

@ -0,0 +1,173 @@
<script>
/* eslint no-console:0 */
import Tree, { TreeNode } from '../index'
import '../assets/index.less'
import { gData,
/* filterParentPosition, getFilterExpandedKeys,*/ getRadioSelectKeys } from './util'
import '../../vc-dialog/assets/index.less'
import Modal from '../../vc-dialog'
import BaseMixin from '../../_util/BaseMixin'
export default {
mixins: [BaseMixin],
data () {
return {
// expandedKeys: getFilterExpandedKeys(gData, ['0-0-0-key']),
expandedKeys: ['0-0-0-key'],
autoExpandParent: true,
// checkedKeys: ['0-0-0-0-key', '0-0-1-0-key', '0-1-0-0-key'],
checkedKeys: ['0-0-0-key'],
checkStrictlyKeys: { checked: ['0-0-1-key'], halfChecked: [] },
selectedKeys: [],
treeData: [],
visible: false,
multiple: true,
}
},
methods: {
onExpand (expandedKeys) {
console.log('onExpand', arguments)
// if not set autoExpandParent to false, if children expanded, parent can not collapse.
// or, you can remove all expanded chilren keys.
this.setState({
expandedKeys,
autoExpandParent: false,
})
},
onCheck (checkedKeys) {
this.setState({
checkedKeys,
})
},
onCheckStrictly (checkedKeys, /* extra*/) {
console.log(arguments)
// const { checkedNodesPositions } = extra;
// const pps = filterParentPosition(checkedNodesPositions.map(i => i.pos));
// console.log(checkedNodesPositions.filter(i => pps.indexOf(i.pos) > -1).map(i => i.node.key));
const cks = {
checked: checkedKeys.checked || checkedKeys,
halfChecked: [`0-0-${parseInt(Math.random() * 3, 10)}-key`],
}
this.setState({
// checkedKeys,
checkStrictlyKeys: cks,
// checkStrictlyKeys: checkedKeys,
})
},
onSelect (selectedKeys, info) {
console.log('onSelect', selectedKeys, info)
this.setState({
selectedKeys,
})
},
onRbSelect (selectedKeys, info) {
let _selectedKeys = selectedKeys
if (info.selected) {
_selectedKeys = getRadioSelectKeys(gData, selectedKeys, info.node.props.eventKey)
}
this.setState({
selectedKeys: _selectedKeys,
})
},
onClose () {
this.setState({
visible: false,
})
},
handleOk () {
this.setState({
visible: false,
})
},
showModal () {
this.setState({
expandedKeys: ['0-0-0-key', '0-0-1-key'],
checkedKeys: ['0-0-0-key'],
visible: true,
})
// simulate Ajax
setTimeout(() => {
this.setState({
treeData: [...gData],
})
}, 2000)
},
triggerChecked () {
this.setState({
checkedKeys: [`0-0-${parseInt(Math.random() * 3, 10)}-key`],
})
},
},
render () {
const loop = data => {
return data.map((item) => {
if (item.children) {
return (
<TreeNode
key={item.key} title={item.title}
disableCheckbox={item.key === '0-0-0-key'}
>
{loop(item.children)}
</TreeNode>
)
}
return <TreeNode key={item.key} title={item.title} />
})
}
// console.log(getRadioSelectKeys(gData, this.selectedKeys));
return (<div style={{ padding: '0 20px' }}>
<h2>dialog</h2>
<button class='btn btn-primary' onClick={this.showModal}>show dialog</button>
<Modal
title='TestDemo' visible={this.visible}
onOk={this.handleOk} onClose={this.onClose}
>
{this.treeData.length ? (
<Tree
checkable class='dialog-tree'
onExpand={this.onExpand} expandedKeys={this.expandedKeys}
autoExpandParent={this.autoExpandParent}
onCheck={this.onCheck} checkedKeys={this.checkedKeys}
>
{loop(this.treeData)}
</Tree>
) : 'loading...'}
</Modal>
<h2>controlled</h2>
<Tree
checkable
onExpand={this.onExpand} expandedKeys={this.expandedKeys}
autoExpandParent={this.autoExpandParent}
onCheck={this.onCheck} checkedKeys={this.checkedKeys}
onSelect={this.onSelect} selectedKeys={this.selectedKeys}
>
{loop(gData)}
</Tree>
<button onClick={this.triggerChecked}>trigger checked</button>
<h2>checkStrictly</h2>
<Tree
checkable multiple={this.multiple} defaultExpandAll
onExpand={this.onExpand} expandedKeys={this.expandedKeys}
onCheck={this.onCheckStrictly}
checkedKeys={this.checkStrictlyKeys}
>
{loop(gData)}
</Tree>
<h2>radio's behavior select (in the same level)</h2>
<Tree
multiple defaultExpandAll
onSelect={this.onRbSelect}
selectedKeys={getRadioSelectKeys(gData, this.selectedKeys) }
>
{loop(gData)}
</Tree>
</div>)
},
}
</script>

View File

@ -16,7 +16,6 @@ export default {
defaultSelectedKeys: keys,
defaultCheckedKeys: keys,
switchIt: true,
showMore: false,
}
},
methods: {
@ -55,7 +54,7 @@ export default {
</span>)
return (<div style={{ margin: '0 20px' }}>
<h2>simple</h2>
{/* <Tree
<Tree
class='myCls' showLine checkable defaultExpandAll
defaultExpandedKeys={this.defaultExpandedKeys}
onExpand={this.onExpand}
@ -77,10 +76,9 @@ export default {
<TreeNode title='parent 1-2-1' key='0-0-2-1' />
</TreeNode>
</TreeNode>
</Tree> */}
</Tree>
<h2>Check on Click TreeNode</h2>
<button onClick={this.toggleChildren}>toggle children</button>
<Tree
class='myCls'
showLine
@ -98,10 +96,6 @@ export default {
<TreeNode title='parent 1-1-0' key='0-0-1-0' disableCheckbox />
<TreeNode title='parent 1-1-1' key='0-0-1-1' />
</TreeNode>
{this.showMore ? <TreeNode title='parent 2-1' key='0-0-2'>
<TreeNode title='parent 2-1-0' key='0-0-2-0' disableCheckbox />
<TreeNode title='parent 2-1-1' key='0-0-2-1' />
</TreeNode> : null}
</TreeNode>
</Tree>
</div>)

View File

@ -0,0 +1,176 @@
/* eslint no-loop-func: 0*/
/* eslint no-console:0 */
export function generateData (x = 3, y = 2, z = 1, gData = []) {
// x每一级下的节点总数。y每级节点里有y个节点、存在子节点。z树的level层级数0表示一级
function _loop (_level, _preKey, _tns) {
const preKey = _preKey || '0'
const tns = _tns || gData
const children = []
for (let i = 0; i < x; i++) {
const key = `${preKey}-${i}`
tns.push({ title: `${key}-label`, key: `${key}-key` })
if (i < y) {
children.push(key)
}
}
if (_level < 0) {
return tns
}
const __level = _level - 1
children.forEach((key, index) => {
tns[index].children = []
return _loop(__level, key, tns[index].children)
})
}
_loop(z)
return gData
}
export function calcTotal (x = 3, y = 2, z = 1) {
/* eslint no-param-reassign:0*/
const rec = (n) => n >= 0 ? x * Math.pow(y, n--) + rec(n) : 0
return rec(z + 1)
}
console.log('总节点数单个tree', calcTotal())
// 性能测试:总节点数超过 2000z要小明显感觉慢。z 变大时,递归多,会卡死。
export const gData = generateData()
function isPositionPrefix (smallPos, bigPos) {
if (bigPos.length < smallPos.length) {
return false
}
// attention: "0-0-1" "0-0-10"
if ((bigPos.length > smallPos.length) && (bigPos.charAt(smallPos.length) !== '-')) {
return false
}
return bigPos.substr(0, smallPos.length) === smallPos
}
// console.log(isPositionPrefix("0-1", "0-10-1"));
// arr.length === 628, use time: ~20ms
export function filterParentPosition (arr) {
const levelObj = {}
arr.forEach((item) => {
const posLen = item.split('-').length
if (!levelObj[posLen]) {
levelObj[posLen] = []
}
levelObj[posLen].push(item)
})
const levelArr = Object.keys(levelObj).sort()
for (let i = 0; i < levelArr.length; i++) {
if (levelArr[i + 1]) {
levelObj[levelArr[i]].forEach(ii => {
for (let j = i + 1; j < levelArr.length; j++) {
levelObj[levelArr[j]].forEach((_i, index) => {
if (isPositionPrefix(ii, _i)) {
levelObj[levelArr[j]][index] = null
}
})
levelObj[levelArr[j]] = levelObj[levelArr[j]].filter(p => p)
}
})
}
}
let nArr = []
levelArr.forEach(i => {
nArr = nArr.concat(levelObj[i])
})
return nArr
}
// console.log(filterParentPosition(
// ['0-2', '0-3-3', '0-10', '0-10-0', '0-0-1', '0-0', '0-1-1', '0-1']
// ));
function loopData (data, callback) {
const loop = (d, level = 0) => {
d.forEach((item, index) => {
const pos = `${level}-${index}`
if (item.children) {
loop(item.children, pos)
}
callback(item, index, pos)
})
}
loop(data)
}
function spl (str) {
return str.split('-')
}
function splitLen (str) {
return str.split('-').length
}
export function getFilterExpandedKeys (data, expandedKeys) {
const expandedPosArr = []
loopData(data, (item, index, pos) => {
if (expandedKeys.indexOf(item.key) > -1) {
expandedPosArr.push(pos)
}
})
const filterExpandedKeys = []
loopData(data, (item, index, pos) => {
expandedPosArr.forEach(p => {
if ((splitLen(pos) < splitLen(p) &&
p.indexOf(pos) === 0 || pos === p) &&
filterExpandedKeys.indexOf(item.key) === -1) {
filterExpandedKeys.push(item.key)
}
})
})
return filterExpandedKeys
}
function isSibling (pos, pos1) {
pos.pop()
pos1.pop()
return pos.join(',') === pos1.join(',')
}
export function getRadioSelectKeys (data, selectedKeys, key) {
const res = []
const pkObjArr = []
const selPkObjArr = []
loopData(data, (item, index, pos) => {
if (selectedKeys.indexOf(item.key) > -1) {
pkObjArr.push([pos, item.key])
}
if (key && key === item.key) {
selPkObjArr.push(pos, item.key)
}
})
const lenObj = {}
const getPosKey = (pos, k) => {
const posLen = splitLen(pos)
if (!lenObj[posLen]) {
lenObj[posLen] = [[pos, k]]
} else {
lenObj[posLen].forEach((pkArr, i) => {
if (isSibling(spl(pkArr[0]), spl(pos))) {
// 后来覆盖前者
lenObj[posLen][i] = [pos, k]
} else if (spl(pkArr[0]) !== spl(pos)) {
lenObj[posLen].push([pos, k])
}
})
}
}
pkObjArr.forEach((pk) => {
getPosKey(pk[0], pk[1])
})
if (key) {
getPosKey(selPkObjArr[0], selPkObjArr[1])
}
Object.keys(lenObj).forEach((item) => {
lenObj[item].forEach((i) => {
if (res.indexOf(i[1]) === -1) {
res.push(i[1])
}
})
})
return res
}

View File

@ -23,6 +23,7 @@ export const contextTypes = {
prefixCls: PropTypes.string,
selectable: PropTypes.bool,
showIcon: PropTypes.bool,
icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
draggable: PropTypes.bool,
checkable: PropTypes.oneOfType([
PropTypes.bool,
@ -62,6 +63,7 @@ const Tree = {
prefixCls: PropTypes.string,
showLine: PropTypes.bool,
showIcon: PropTypes.bool,
icon: PropTypes.oneOfType([PropTypes.object, PropTypes.func]),
focusable: PropTypes.bool,
selectable: PropTypes.bool,
disabled: PropTypes.bool,
@ -72,6 +74,7 @@ const Tree = {
]),
checkStrictly: PropTypes.bool,
draggable: PropTypes.bool,
defaultExpandParent: PropTypes.bool,
autoExpandParent: PropTypes.bool,
defaultExpandAll: PropTypes.bool,
defaultExpandedKeys: PropTypes.arrayOf(PropTypes.string),
@ -110,6 +113,7 @@ const Tree = {
disabled: false,
checkStrictly: false,
draggable: false,
defaultExpandParent: true,
autoExpandParent: true,
defaultExpandAll: false,
defaultExpandedKeys: [],
@ -123,15 +127,31 @@ const Tree = {
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 = {
@ -141,13 +161,7 @@ const Tree = {
halfCheckedKeys: 'sHalfCheckedKeys',
}
return {
sExpandedKeys: defaultExpandAll
? getFullKeyList(children)
: calcExpandedKeys(defaultExpandedKeys, props, children),
sSelectedKeys: calcSelectedKeys(defaultSelectedKeys, props, children),
sCheckedKeys: checkedKeys,
sHalfCheckedKeys: halfCheckedKeys,
...state,
...(this.getSyncProps(props) || {}),
dragOverNodeKey: '',
dropPosition: null,
@ -161,18 +175,21 @@ const Tree = {
watch: {
children (val) {
const { checkedKeys = [], halfCheckedKeys = [] } = calcCheckedKeys(this.checkedKeys || this.sCheckedKeys, this.$props, this.$slots.default) || {}
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 = calcExpandedKeys(this.expandedKeys, this.$props, this.$slots.default)
this.sExpandedKeys = this.autoExpandParent ? calcExpandedKeys(val, this.$props, this.$slots.default) : val
},
selectedKeys (val) {
this.sSelectedKeys = calcSelectedKeys(this.selectedKeys, this.$props, this.$slots.default)
this.sSelectedKeys = calcSelectedKeys(val, this.$props, this.$slots.default)
},
checkedKeys (val) {
const { checkedKeys = [], halfCheckedKeys = [] } = calcCheckedKeys(this.checkedKeys, this.$props, this.$slots.default) || {}
const { checkedKeys = [], halfCheckedKeys = [] } = calcCheckedKeys(val, this.$props, this.$slots.default) || {}
this.sCheckedKeys = checkedKeys
this.sHalfCheckedKeys = halfCheckedKeys
},
@ -251,6 +268,18 @@ const Tree = {
}, 0)
},
onNodeDragOver (event, node) {
const { eventKey } = node.props
// Update drag position
if (this.dragNode && eventKey === this.dragOverNodeKey) {
const dropPosition = calcDropPosition(event, node)
if (dropPosition === this.dropPosition) return
this.setState({
dropPosition,
})
}
this.__emit('dragover', { event, node })
},
onNodeDragLeave (event, node) {
@ -502,8 +531,10 @@ const Tree = {
newState.sHalfCheckedKeys = halfCheckedKeys
}
if (checkSync('expandedKeys')) {
newState.sExpandedKeys = calcExpandedKeys(props.expandedKeys, props, children)
// Re-calculate when autoExpandParent or expandedKeys changed
if (prevProps && (checkSync('autoExpandParent') || checkSync('expandedKeys'))) {
newState.sExpandedKeys = props.autoExpandParent
? calcExpandedKeys(props.expandedKeys, props, children) : props.expandedKeys
}
if (checkSync('selectedKeys')) {

View File

@ -65,10 +65,10 @@ const TreeNode = {
},
inject: {
vcTree: { default: {}},
vcTreeNode: { default: {}},
},
provide () {
return {
vcTree: this.vcTree,
vcTreeNode: this,
}
},
@ -358,19 +358,37 @@ const TreeNode = {
// Load data to avoid default expanded tree without data
syncLoadData (props) {
const { loadStatus } = this
const { expanded } = props
const { expanded } = this
const { vcTree: { loadData }} = this
if (loadData && loadStatus === LOAD_STATUS_NONE && expanded && !this.isLeaf2()) {
this.setState({ loadStatus: LOAD_STATUS_LOADING })
// read from state to avoid loadData at same time
this.setState(({ loadStatus }) => {
if (loadData && loadStatus === LOAD_STATUS_NONE && expanded && !this.isLeaf()) {
loadData(this).then(() => {
this.setState({ loadStatus: LOAD_STATUS_LOADED })
}).catch(() => {
this.setState({ loadStatus: LOAD_STATUS_FAILED })
})
loadData(this).then(() => {
this.setState({ loadStatus: LOAD_STATUS_LOADED })
}).catch(() => {
this.setState({ loadStatus: LOAD_STATUS_FAILED })
})
}
return { loadStatus: LOAD_STATUS_LOADING }
}
return null
})
// const { loadStatus } = this
// const { expanded } = props
// const { vcTree: { loadData }} = this
// if (loadData && loadStatus === LOAD_STATUS_NONE && expanded && !this.isLeaf2()) {
// this.setState({ loadStatus: LOAD_STATUS_LOADING })
// loadData(this).then(() => {
// this.setState({ loadStatus: LOAD_STATUS_LOADED })
// }).catch(() => {
// this.setState({ loadStatus: LOAD_STATUS_FAILED })
// })
// }
},
// Switcher
@ -437,7 +455,7 @@ const TreeNode = {
// Icon + Title
renderSelector () {
const { title, selected, icon, loadStatus, dragNodeHighlight } = this
const { vcTree: { prefixCls, showIcon, draggable, loadData }} = this
const { vcTree: { prefixCls, showIcon, icon: treeIcon, draggable, loadData }} = this
const disabled = this.isDisabled()
const wrapClass = `${prefixCls}-node-content-wrapper`
@ -446,15 +464,18 @@ const TreeNode = {
let $icon
if (showIcon) {
$icon = icon ? (
const currentIcon = icon || treeIcon
$icon = currentIcon ? (
<span
class={classNames(
`${prefixCls}-iconEle`,
`${prefixCls}-icon__customize`,
)}
>
{typeof icon === 'function'
? icon(this.$props) : icon}
{typeof currentIcon === 'function'
? currentIcon(
{ props: this.$props, on: this.$listeners }
) : currentIcon}
</span>
) : this.renderIcon()
} else if (loadData && loadStatus === LOAD_STATUS_LOADING) {
@ -510,6 +531,7 @@ const TreeNode = {
animProps = getTransitionProps(openTransitionName, { appear: transitionAppear })
} else if (typeof openAnimation === 'object') {
animProps = { ...openAnimation }
animProps.props = { css: false, ...animProps.props }
if (!transitionAppear) {
delete animProps.props.appear
}
@ -526,7 +548,6 @@ const TreeNode = {
if (expanded) {
$children = (
<ul
v-show={expanded}
class={classNames(
`${prefixCls}-child-tree`,
expanded && `${prefixCls}-child-tree-open`,

View File

@ -1,6 +1,8 @@
/* 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
export function arrDel (list, value) {
const clone = list.slice()
@ -23,27 +25,6 @@ export function posToArr (pos) {
return pos.split('-')
}
// Only used when drag, not affect SSR.
export function getOffset (ele) {
if (!ele.getClientRects().length) {
return { top: 0, left: 0 }
}
const rect = ele.getBoundingClientRect()
if (rect.width || rect.height) {
const doc = ele.ownerDocument
const win = doc.defaultView
const docElem = doc.documentElement
return {
top: rect.top + win.pageYOffset - docElem.clientTop,
left: rect.left + win.pageXOffset - docElem.clientLeft,
}
}
return rect
}
export function getPosition (level, index) {
return `${level}-${index}`
}
@ -194,15 +175,14 @@ export function getDragNodesKeys (treeNodes, node) {
}
export function calcDropPosition (event, treeNode) {
const offsetTop = getOffset(treeNode.selectHandle).top
const offsetHeight = treeNode.selectHandle.offsetHeight
const pageY = event.pageY
const gapHeight = 2 // [Legacy] TODO: remove hard code
if (pageY > offsetTop + offsetHeight - gapHeight) {
return 1
}
if (pageY < offsetTop + gapHeight) {
const { clientY } = event
const { top, bottom, height } = treeNode.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
}
@ -218,13 +198,6 @@ export function calcExpandedKeys (keyList, props, children = []) {
return []
}
const { autoExpandParent } = props
// Do nothing if not auto expand parent
if (!autoExpandParent) {
return keyList
}
// Fill parent expanded keys
const { keyNodes, nodeList } = getNodesStatistic(children)
const needExpandKeys = {}
@ -330,10 +303,11 @@ export function calcCheckStateConduct (treeNodes, checkedKeys) {
}
const { subNodes = [], parentPos, node } = keyNodes[key]
if (isCheckDisabled(node)) return
tgtCheckedKeys[key] = true
if (isCheckDisabled(node)) return
// Conduct down
subNodes
.filter(sub => !isCheckDisabled(sub.node))

View File

@ -3,7 +3,7 @@ import Layout from './components/layout.vue'
const AsyncTestComp = () => {
const d = window.location.hash.replace('#', '')
return {
component: import(`../components/transfer/demo/${d}`),
component: import(`../components/vc-tree/demo/${d}`),
}
}