pull/165/head
tjz 7 years ago
parent 62964ceb6f
commit 5ea54b104a

@ -20,7 +20,6 @@ export default {
},
inject: {
checkboxGroupContext: { default: null },
test: { default: null },
},
data () {
const { checkboxGroupContext, checked, defaultChecked, value } = this

@ -8,7 +8,7 @@
| getPopupContainer | to set the container of the dropdown menu. The default is to create a `div` element in `body`, you can reset it to the scrolling area and make a relative reposition. [example](https://codepen.io/afc163/pen/zEjNOy?editors=0010) | Function(triggerNode) | `() => document.body` |
| overlay(slot) | the dropdown menu | [Menu](#/us/components/menu) | - |
| placement | placement of pop menu: `bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | String | `bottomLeft` |
| trigger | the trigger mode which executes the drop-down action | Array<`click`\|`hover`\|`contextMenu`> | `['hover']` |
| trigger | the trigger mode which executes the drop-down action | Array<`click`\|`hover`\|`contextmenu`> | `['hover']` |
| visible(v-model) | whether the dropdown menu is visible | boolean | - |
### events
@ -30,7 +30,7 @@ You should use [Menu](#/us/components/menu/) as `overlay`. The menu items and di
| overlay(slot) | the dropdown menu | [Menu](#/us/components/menu) | - |
| placement | placement of pop menu: `bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | String | `bottomLeft` |
| size | size of the button, the same as [Button](#/us/components/button) | string | `default` |
| trigger | the trigger mode which executes the drop-down action | Array<`click`\|`hover`\|`contextMenu`> | `['hover']` |
| trigger | the trigger mode which executes the drop-down action | Array<`click`\|`hover`\|`contextmenu`> | `['hover']` |
| type | type of the button, the same as [Button](#/us/components/button) | string | `default` |
| visible | whether the dropdown menu is visible | boolean | - |

@ -8,7 +8,7 @@
| getPopupContainer | 菜单渲染父节点。默认渲染到 body 上,如果你遇到菜单滚动定位问题,试试修改为滚动的区域,并相对其定位。 | Function(triggerNode) | `() => document.body` |
| overlay(slot) | 菜单 | [Menu](#/cn/components/menu) | - |
| placement | 菜单弹出位置:`bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | String | `bottomLeft` |
| trigger | 触发下拉的行为 | Array<`click`\|`hover`\|`contextMenu`> | `['hover']` |
| trigger | 触发下拉的行为 | Array<`click`\|`hover`\|`contextmenu`> | `['hover']` |
| visible(v-model) | 菜单是否显示 | boolean | - |
`overlay` 菜单使用 [Menu](#/cn/components/menu/),还包括菜单项 `Menu.Item`,分割线 `Menu.Divider`
@ -31,7 +31,7 @@
| overlay(slot) | 菜单 | [Menu](#/cn/components/menu/) | - |
| placement | 菜单弹出位置:`bottomLeft` `bottomCenter` `bottomRight` `topLeft` `topCenter` `topRight` | String | `bottomLeft` |
| size | 按钮大小,和 [Button](#/cn/components/button/) 一致 | string | 'default' |
| trigger | 触发下拉的行为 | Array<`click`\|`hover`\|`contextMenu`> | `['hover']` |
| trigger | 触发下拉的行为 | Array<`click`\|`hover`\|`contextmenu`> | `['hover']` |
| type | 按钮类型,和 [Button](#/cn/components/button/) 一致 | string | 'default' |
| visible(v-model) | 菜单是否显示 | boolean | - |

@ -40,7 +40,7 @@
<td>action</td>
<td>string[]</td>
<td>['hover']</td>
<td>which actions cause popup shown. enum of 'hover','click','focus','contextMenu'</td>
<td>which actions cause popup shown. enum of 'hover','click','focus','contextmenu'</td>
</tr>
<tr>
<td>mouseEnterDelay</td>

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 381 B

@ -0,0 +1,3 @@
'use strict'
module.exports = require('./src/')

@ -0,0 +1,610 @@
import PropTypes from '../../_util/vue-types'
import classNames from 'classnames'
import warning from 'warning'
import { initDefaultProps, getOptionProps, filterEmpty } from '../../_util/props-util'
import {
traverseTreeNodes, getStrictlyValue,
getFullKeyList, getPosition, getDragNodesKeys,
calcExpandedKeys, calcSelectedKeys,
calcCheckedKeys, calcDropPosition,
arrAdd, arrDel, posToArr,
} from './util'
/**
* Thought we still use `cloneElement` to pass `key`,
* other props can pass with context for future refactor.
*/
export const contextTypes = {
rcTree: PropTypes.shape({
root: PropTypes.object,
prefixCls: PropTypes.string,
selectable: PropTypes.bool,
showIcon: PropTypes.bool,
draggable: PropTypes.bool,
checkable: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.object,
]),
checkStrictly: PropTypes.bool,
disabled: PropTypes.bool,
openTransitionName: PropTypes.string,
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
loadData: PropTypes.func,
filterTreeNode: PropTypes.func,
renderTreeNode: PropTypes.func,
isKeyChecked: PropTypes.func,
// onNodeExpand: PropTypes.func,
// onNodeSelect: PropTypes.func,
// onNodeMouseEnter: PropTypes.func,
// onNodeMouseLeave: PropTypes.func,
// onNodeContextMenu: PropTypes.func,
// onNodeDragStart: PropTypes.func,
// onNodeDragEnter: PropTypes.func,
// onNodeDragOver: PropTypes.func,
// onNodeDragLeave: PropTypes.func,
// onNodeDragEnd: PropTypes.func,
// onNodeDrop: PropTypes.func,
// onBatchNodeCheck: PropTypes.func,
// onCheckConductFinished: PropTypes.func,
}),
}
const Tree = {
props: initDefaultProps({
prefixCls: PropTypes.string,
showLine: PropTypes.bool,
showIcon: PropTypes.bool,
focusable: PropTypes.bool,
selectable: PropTypes.bool,
disabled: PropTypes.bool,
multiple: PropTypes.bool,
checkable: PropTypes.oneOfType([
PropTypes.bool,
PropTypes.node,
]),
checkStrictly: PropTypes.bool,
draggable: PropTypes.bool,
autoExpandParent: PropTypes.bool,
defaultExpandAll: PropTypes.bool,
defaultExpandedKeys: PropTypes.arrayOf(PropTypes.string),
expandedKeys: PropTypes.arrayOf(PropTypes.string),
defaultCheckedKeys: PropTypes.arrayOf(PropTypes.string),
checkedKeys: PropTypes.oneOfType([
PropTypes.arrayOf(PropTypes.string),
PropTypes.object,
]),
defaultSelectedKeys: PropTypes.arrayOf(PropTypes.string),
selectedKeys: PropTypes.arrayOf(PropTypes.string),
//onExpand: PropTypes.func,
//onCheck: PropTypes.func,
//onSelect: PropTypes.func,
loadData: PropTypes.func,
// onMouseEnter: PropTypes.func,
// onMouseLeave: PropTypes.func,
// onRightClick: PropTypes.func,
// onDragStart: PropTypes.func,
// onDragEnter: PropTypes.func,
// onDragOver: PropTypes.func,
// onDragLeave: PropTypes.func,
// onDragEnd: PropTypes.func,
// onDrop: PropTypes.func,
filterTreeNode: PropTypes.func,
openTransitionName: PropTypes.string,
openAnimation: PropTypes.oneOfType([PropTypes.string, PropTypes.object]),
}, {
prefixCls: 'rc-tree',
showLine: false,
showIcon: true,
selectable: true,
multiple: false,
checkable: false,
disabled: false,
checkStrictly: false,
draggable: false,
autoExpandParent: true,
defaultExpandAll: false,
defaultExpandedKeys: [],
defaultCheckedKeys: [],
defaultSelectedKeys: [],
}),
// static childContextTypes = contextTypes;
data () {
const props = getOptionProps(this)
const {
defaultExpandAll,
defaultExpandedKeys,
defaultCheckedKeys,
defaultSelectedKeys,
} = props
// Sync state with props
const { checkedKeys = [], halfCheckedKeys = [] } =
calcCheckedKeys(defaultCheckedKeys, props) || {}
// Cache for check status to optimize
this.checkedBatch = null
return {
sExpandedKeys: defaultExpandAll
? getFullKeyList(this.$slots.default)
: calcExpandedKeys(defaultExpandedKeys, props),
sSelectedKeys: calcSelectedKeys(defaultSelectedKeys, props),
sCheckedKeys,
sHalfCheckedKeys,
...(this.getSyncProps(props) || {}),
}
},
provide: {
rcTree: this,
},
componentWillReceiveProps (nextProps) {
// React 16 will not trigger update if new state is null
this.setState(this.getSyncProps(nextProps, this.props))
},
onNodeDragStart (event, node) {
const { expandedKeys } = this.state
const { onDragStart } = this.props
const { eventKey, children } = node.props
this.dragNode = node
this.setState({
dragNodesKeys: getDragNodesKeys(children, node),
expandedKeys: arrDel(expandedKeys, eventKey),
})
if (onDragStart) {
onDragStart({ event, node })
}
},
/**
* [Legacy] Select handler is less small than node,
* so that this will trigger when drag enter node or select handler.
* This is a little tricky if customize css without padding.
* Better for use mouse move event to refresh drag state.
* But let's just keep it to avoid event trigger logic change.
*/
onNodeDragEnter = (event, node) => {
const { expandedKeys } = this.state
const { onDragEnter } = this.props
const { pos, eventKey } = node.props
const dropPosition = calcDropPosition(event, node)
// Skip if drag node is self
if (
this.dragNode.props.eventKey === eventKey &&
dropPosition === 0
) {
this.setState({
dragOverNodeKey: '',
dropPosition: null,
})
return
}
// Ref: https://github.com/react-component/tree/issues/132
// Add timeout to let onDragLevel fire before onDragEnter,
// so that we can clean drag props for onDragLeave node.
// Macro task for this:
// https://html.spec.whatwg.org/multipage/webappapis.html#clean-up-after-running-script
setTimeout(() => {
// Update drag over node
this.setState({
dragOverNodeKey: eventKey,
dropPosition,
})
// Side effect for delay drag
if (!this.delayedDragEnterLogic) {
this.delayedDragEnterLogic = {}
}
Object.keys(this.delayedDragEnterLogic).forEach((key) => {
clearTimeout(this.delayedDragEnterLogic[key])
})
this.delayedDragEnterLogic[pos] = setTimeout(() => {
const newExpandedKeys = arrAdd(expandedKeys, eventKey)
this.setState({
expandedKeys: newExpandedKeys,
})
if (onDragEnter) {
onDragEnter({ event, node, expandedKeys: newExpandedKeys })
}
}, 400)
}, 0)
};
onNodeDragOver = (event, node) => {
const { onDragOver } = this.props
if (onDragOver) {
onDragOver({ event, node })
}
};
onNodeDragLeave = (event, node) => {
const { onDragLeave } = this.props
this.setState({
dragOverNodeKey: '',
})
if (onDragLeave) {
onDragLeave({ event, node })
}
};
onNodeDragEnd = (event, node) => {
const { onDragEnd } = this.props
this.setState({
dragOverNodeKey: '',
})
if (onDragEnd) {
onDragEnd({ event, node })
}
};
onNodeDrop = (event, node) => {
const { dragNodesKeys, dropPosition } = this.state
const { onDrop } = this.props
const { eventKey, pos } = node.props
this.setState({
dragOverNodeKey: '',
dropNodeKey: eventKey,
})
if (dragNodesKeys.indexOf(eventKey) !== -1) {
warning(false, 'Can not drop to dragNode(include it\'s children node)')
return
}
const posArr = posToArr(pos)
const dropResult = {
event,
node,
dragNode: this.dragNode,
dragNodesKeys: dragNodesKeys.slice(),
dropPosition: dropPosition + Number(posArr[posArr.length - 1]),
}
if (dropPosition !== 0) {
dropResult.dropToGap = true
}
if (onDrop) {
onDrop(dropResult)
}
};
onNodeSelect = (e, treeNode) => {
let { selectedKeys } = this.state
const { onSelect, multiple, children } = this.props
const { selected, eventKey } = treeNode.props
const targetSelected = !selected
// Update selected keys
if (!targetSelected) {
selectedKeys = arrDel(selectedKeys, eventKey)
} else if (!multiple) {
selectedKeys = [eventKey]
} else {
selectedKeys = arrAdd(selectedKeys, eventKey)
}
// [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)
}
})
}
this.setUncontrolledState({ selectedKeys })
if (onSelect) {
const eventObj = {
event: 'select',
selected: targetSelected,
node: treeNode,
selectedNodes,
}
onSelect(selectedKeys, eventObj)
}
};
/**
* 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 = () => {
const { checkedKeys, halfCheckedKeys } = this.state
const { onCheck, checkStrictly, children } = this.props
// Use map to optimize update speed
const checkedKeySet = {}
const halfCheckedKeySet = {}
checkedKeys.forEach(key => {
checkedKeySet[key] = true
})
halfCheckedKeys.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
const eventObj = {
event: 'check',
node: this.checkedBatch.treeNode,
checked: this.checkedBatch.checked,
}
if (checkStrictly) {
selectedObj = getStrictlyValue(newCheckedKeys, newHalfCheckedKeys)
// [Legacy] TODO: add optimize prop to skip node process
eventObj.checkedNodes = []
traverseTreeNodes(children, ({ node, key }) => {
if (checkedKeySet[key]) {
eventObj.checkedNodes.push(node)
}
})
this.setUncontrolledState({ checkedKeys: newCheckedKeys })
} else {
selectedObj = newCheckedKeys
// [Legacy] TODO: add optimize prop to skip node process
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 })
}
})
this.setUncontrolledState({
checkedKeys: newCheckedKeys,
halfCheckedKeys: newHalfCheckedKeys,
})
}
if (onCheck) {
onCheck(selectedObj, eventObj)
}
// Clean up
this.checkedBatch = null
};
onNodeExpand = (e, treeNode) => {
let { expandedKeys } = this.state
const { onExpand, loadData } = this.props
const { eventKey, expanded } = treeNode.props
// Update selected keys
const index = expandedKeys.indexOf(eventKey)
const targetExpanded = !expanded
warning(
(expanded && index !== -1) || (!expanded && index === -1)
, 'Expand state not sync with index check')
if (targetExpanded) {
expandedKeys = arrAdd(expandedKeys, eventKey)
} else {
expandedKeys = arrDel(expandedKeys, eventKey)
}
this.setUncontrolledState({ expandedKeys })
if (onExpand) {
onExpand(expandedKeys, { node: treeNode, expanded: targetExpanded })
}
// Async Load data
if (targetExpanded && loadData) {
return loadData(treeNode).then(() => {
// [Legacy] Refresh logic
this.setUncontrolledState({ expandedKeys })
})
}
return null
};
onNodeMouseEnter = (event, node) => {
const { onMouseEnter } = this.props
if (onMouseEnter) {
onMouseEnter({ event, node })
}
};
onNodeMouseLeave = (event, node) => {
const { onMouseLeave } = this.props
if (onMouseLeave) {
onMouseLeave({ event, node })
}
};
onNodeContextMenu = (event, node) => {
const { onRightClick } = this.props
if (onRightClick) {
event.preventDefault()
onRightClick({ event, node })
}
};
/**
* Sync state with props if needed
*/
getSyncProps = (props = {}, prevProps) => {
let needSync = false
const newState = {}
const myPrevProps = prevProps || {}
function checkSync (name) {
if (props[name] !== myPrevProps[name]) {
needSync = true
return true
}
return false
}
// Children change will affect check box status.
// And no need to check when prev props not provided
if (prevProps && checkSync('children')) {
const { checkedKeys = [], halfCheckedKeys = [] } =
calcCheckedKeys(props.checkedKeys || this.state.checkedKeys, props) || {}
newState.checkedKeys = checkedKeys
newState.halfCheckedKeys = halfCheckedKeys
}
if (checkSync('expandedKeys')) {
newState.expandedKeys = calcExpandedKeys(props.expandedKeys, props)
}
if (checkSync('selectedKeys')) {
newState.selectedKeys = calcSelectedKeys(props.selectedKeys, props)
}
if (checkSync('checkedKeys')) {
const { checkedKeys = [], halfCheckedKeys = [] } =
calcCheckedKeys(props.checkedKeys, props) || {}
newState.checkedKeys = checkedKeys
newState.halfCheckedKeys = halfCheckedKeys
}
return needSync ? newState : null
};
/**
* Only update the value which is not in props
*/
setUncontrolledState = (state) => {
let needSync = false
const newState = {}
Object.keys(state).forEach(name => {
if (name in this.props) return
needSync = true
newState[name] = state[name]
})
this.setState(needSync ? newState : null)
};
isKeyChecked = (key) => {
const { checkedKeys = [] } = this.state
return checkedKeys.indexOf(key) !== -1
};
/**
* [Legacy] Original logic use `key` as tracking clue.
* We have to use `cloneElement` to pass `key`.
*/
renderTreeNode = (child, index, level = 0) => {
const {
expandedKeys = [], selectedKeys = [], halfCheckedKeys = [],
dragOverNodeKey, dropPosition,
} = this.state
const {} = this.props
const pos = getPosition(level, index)
const key = child.key || pos
return React.cloneElement(child, {
eventKey: key,
expanded: expandedKeys.indexOf(key) !== -1,
selected: selectedKeys.indexOf(key) !== -1,
checked: this.isKeyChecked(key),
halfChecked: halfCheckedKeys.indexOf(key) !== -1,
pos,
// [Legacy] Drag props
dragOver: dragOverNodeKey === key && dropPosition === 0,
dragOverGapTop: dragOverNodeKey === key && dropPosition === -1,
dragOverGapBottom: dragOverNodeKey === key && dropPosition === 1,
})
};
render () {
const {
prefixCls, className, focusable,
showLine,
children,
} = this.props
const domProps = {}
// [Legacy] Commit: 0117f0c9db0e2956e92cb208f51a42387dfcb3d1
if (focusable) {
domProps.tabIndex = '0'
domProps.onKeyDown = this.onKeyDown
}
return (
<ul
{...domProps}
className={classNames(prefixCls, className, {
[`${prefixCls}-show-line`]: showLine,
})}
role='tree-node'
unselectable='on'
>
{React.Children.map(children, this.renderTreeNode, this)}
</ul>
)
}
}
export default Tree

@ -0,0 +1,585 @@
import PropTypes from '../../_util/vue-types'
import classNames from 'classnames'
import warning from 'warning'
import { contextTypes } from './Tree'
import { getPosition, getNodeChildren, isCheckDisabled, traverseTreeNodes } from './util'
import { initDefaultProps, getOptionProps, filterEmpty } from '../../_util/props-util'
const ICON_OPEN = 'open'
const ICON_CLOSE = 'close'
const LOAD_STATUS_NONE = 0
const LOAD_STATUS_LOADING = 1
const LOAD_STATUS_LOADED = 2
const LOAD_STATUS_FAILED = 0 // Action align, let's make failed same as init.
const defaultTitle = '---'
let onlyTreeNodeWarned = false // Only accept TreeNode
export const nodeContextTypes = {
...contextTypes,
rcTreeNode: PropTypes.shape({
onUpCheckConduct: PropTypes.func,
}),
}
const TreeNode = {
props: initDefaultProps({
eventKey: PropTypes.string, // Pass by parent `cloneElement`
prefixCls: PropTypes.string,
// className: PropTypes.string,
root: PropTypes.object,
// onSelect: PropTypes.func,
// By parent
expanded: PropTypes.bool,
selected: PropTypes.bool,
checked: PropTypes.bool,
halfChecked: PropTypes.bool,
title: PropTypes.node,
pos: PropTypes.string,
dragOver: PropTypes.bool,
dragOverGapTop: PropTypes.bool,
dragOverGapBottom: PropTypes.bool,
// By user
isLeaf: PropTypes.bool,
selectable: PropTypes.bool,
disabled: PropTypes.bool,
disableCheckbox: PropTypes.bool,
icon: PropTypes.any,
}, {
title: defaultTitle,
}),
data () {
return {
loadStatus: LOAD_STATUS_NONE,
dragNodeHighlight: false,
}
},
inject: {
context: { default: {}},
},
provide: {
...this.context,
rcTreeNode: this,
},
// Isomorphic needn't load data in server side
mounted () {
this.$nextTick(() => {
this.syncLoadData(this.$props)
})
},
componentWillReceiveProps (nextProps) {
this.syncLoadData(nextProps)
},
onUpCheckConduct (treeNode, nodeChecked, nodeHalfChecked) {
const { pos: nodePos } = getOptionProps(treeNode)
const { eventKey, pos, checked, halfChecked } = this
const {
rcTree: { checkStrictly, isKeyChecked, onBatchNodeCheck, onCheckConductFinished },
rcTreeNode: { onUpCheckConduct } = {},
} = this.context
// Stop conduct when current node is disabled
if (isCheckDisabled(this)) {
onCheckConductFinished()
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)
} else {
// Flush all the update
onCheckConductFinished()
}
} else {
// Flush all the update
onCheckConductFinished()
}
},
onDownCheckConduct (nodeChecked) {
const { $slots } = this
const children = $slots.default || []
const { rcTree: { checkStrictly, isKeyChecked, onBatchNodeCheck }} = this.context
if (checkStrictly) return
traverseTreeNodes(children, ({ node, key }) => {
if (isCheckDisabled(node)) return false
if (nodeChecked !== isKeyChecked(key)) {
onBatchNodeCheck(key, nodeChecked, false)
}
})
},
onSelectorClick (e) {
if (this.isSelectable()) {
this.onSelect(e)
} else {
this.onCheck(e)
}
},
onSelect (e) {
if (this.isDisabled()) return
const { rcTree: { onNodeSelect }} = this.context
e.preventDefault()
onNodeSelect(e, this)
},
onCheck (e) {
if (this.isDisabled()) return
const { disableCheckbox, checked, eventKey } = this
const {
rcTree: { checkable, onBatchNodeCheck, onCheckConductFinished },
rcTreeNode: { onUpCheckConduct } = {},
} = this.context
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)
} else {
onCheckConductFinished()
}
},
onMouseEnter (e) {
const { rcTree: { onNodeMouseEnter }} = this.context
onNodeMouseEnter(e, this)
},
onMouseLeave (e) {
const { rcTree: { onNodeMouseLeave }} = this.context
onNodeMouseLeave(e, this)
},
onContextMenu (e) {
const { rcTree: { onNodeContextMenu }} = this.context
onNodeContextMenu(e, this)
},
onDragStart (e) {
const { rcTree: { onNodeDragStart }} = this.context
e.stopPropagation()
this.setState({
dragNodeHighlight: true,
})
onNodeDragStart(e, this)
try {
// ie throw error
// firefox-need-it
e.dataTransfer.setData('text/plain', '')
} catch (error) {
// empty
}
},
onDragEnter (e) {
const { rcTree: { onNodeDragEnter }} = this.context
e.preventDefault()
e.stopPropagation()
onNodeDragEnter(e, this)
},
onDragOver (e) {
const { rcTree: { onNodeDragOver }} = this.context
e.preventDefault()
e.stopPropagation()
onNodeDragOver(e, this)
},
onDragLeave (e) {
const { rcTree: { onNodeDragLeave }} = this.context
e.stopPropagation()
onNodeDragLeave(e, this)
},
onDragEnd (e) {
const { rcTree: { onNodeDragEnd }} = this.context
e.stopPropagation()
this.setState({
dragNodeHighlight: false,
})
onNodeDragEnd(e, this)
},
onDrop (e) {
const { rcTree: { onNodeDrop }} = this.context
e.preventDefault()
e.stopPropagation()
this.setState({
dragNodeHighlight: false,
})
onNodeDrop(e, this)
},
// Disabled item still can be switch
onExpand (e) {
const { rcTree: { onNodeExpand }} = this.context
const callbackPromise = onNodeExpand(e, this)
// Promise like
if (callbackPromise && callbackPromise.then) {
this.setState({ loadStatus: LOAD_STATUS_LOADING })
callbackPromise.then(() => {
this.setState({ loadStatus: LOAD_STATUS_LOADED })
}).catch(() => {
this.setState({ loadStatus: LOAD_STATUS_FAILED })
})
}
},
// Drag usage
setSelectHandle (node) {
this.selectHandle = node
},
getNodeChildren () {
const { $slots: { default: children }} = this
const originList = filterEmpty(children)
const targetList = getNodeChildren(originList)
if (originList.length !== targetList.length && !onlyTreeNodeWarned) {
onlyTreeNodeWarned = true
warning(false, 'Tree only accept TreeNode as children.')
}
return targetList
},
getNodeState () {
const { expanded } = this
if (this.isLeaf()) {
return null
}
return expanded ? ICON_OPEN : ICON_CLOSE
},
isLeaf () {
const { isLeaf, loadStatus } = this
const { rcTree: { loadData }} = this.context
const hasChildren = this.getNodeChildren().length !== 0
return (
isLeaf ||
(!loadData && !hasChildren) ||
(loadData && loadStatus === LOAD_STATUS_LOADED && !hasChildren)
)
},
isDisabled () {
const { disabled } = this
const { rcTree: { disabled: treeDisabled }} = this.context
// Follow the logic of Selectable
if (disabled === false) {
return false
}
return !!(treeDisabled || disabled)
},
isSelectable () {
const { selectable } = this
const { rcTree: { selectable: treeSelectable }} = this.context
// Ignore when selectable is undefined or null
if (typeof selectable === 'boolean') {
return selectable
}
return treeSelectable
},
// Load data to avoid default expanded tree without data
syncLoadData (props) {
const { loadStatus } = this
const { expanded } = props
const { rcTree: { loadData }} = this.context
if (loadData && loadStatus === LOAD_STATUS_NONE && expanded && !this.isLeaf()) {
this.setState({ loadStatus: LOAD_STATUS_LOADING })
loadData(this).then(() => {
this.setState({ loadStatus: LOAD_STATUS_LOADED })
}).catch(() => {
this.setState({ loadStatus: LOAD_STATUS_FAILED })
})
}
},
// Switcher
renderSwitcher () {
const { expanded } = this
const { rcTree: { prefixCls }} = this.context
if (this.isLeaf()) {
return <span class={`${prefixCls}-switcher ${prefixCls}-switcher-noop`} />
}
return (
<span
class={classNames(
`${prefixCls}-switcher`,
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
)}
onClick={this.onExpand}
/>
)
},
// Checkbox
renderCheckbox () {
const { checked, halfChecked, disableCheckbox } = this
const { rcTree: { prefixCls, checkable }} = this.context
const disabled = this.isDisabled()
if (!checkable) return null
// [Legacy] Custom element should be separate with `checkable` in future
const $custom = typeof checkable !== 'boolean' ? checkable : null
return (
<span
class={classNames(
`${prefixCls}-checkbox`,
checked && `${prefixCls}-checkbox-checked`,
!checked && halfChecked && `${prefixCls}-checkbox-indeterminate`,
(disabled || disableCheckbox) && `${prefixCls}-checkbox-disabled`,
)}
onClick={this.onCheck}
>
{$custom}
</span>
)
},
renderIcon () {
const { loadStatus } = this
const { rcTree: { prefixCls }} = this.context
return (
<span
class={classNames(
`${prefixCls}-iconEle`,
`${prefixCls}-icon__${this.getNodeState() || 'docu'}`,
(loadStatus === LOAD_STATUS_LOADING) && `${prefixCls}-icon_loading`,
)}
/>
)
},
// Icon + Title
renderSelector () {
const { title, selected, icon, loadStatus, dragNodeHighlight } = this
const { rcTree: { prefixCls, showIcon, draggable, loadData }} = this.context
const disabled = this.isDisabled()
const wrapClass = `${prefixCls}-node-content-wrapper`
// Icon - Still show loading icon when loading without showIcon
let $icon
if (showIcon) {
$icon = icon ? (
<span
class={classNames(
`${prefixCls}-iconEle`,
`${prefixCls}-icon__customize`,
)}
>
{typeof icon === 'function'
? icon(this.$props) : icon}
</span>
) : this.renderIcon()
} else if (loadData && loadStatus === LOAD_STATUS_LOADING) {
$icon = this.renderIcon()
}
// Title
const $title = <span class={`${prefixCls}-title`}>{title}</span>
return (
<span
ref='selectHandle'
title={typeof title === 'string' ? title : ''}
class={classNames(
`${wrapClass}`,
`${wrapClass}-${this.getNodeState() || 'normal'}`,
(!disabled && (selected || dragNodeHighlight)) && `${prefixCls}-node-selected`,
(!disabled && draggable) && 'draggable'
)}
draggable={(!disabled && draggable) || undefined}
aria-grabbed={(!disabled && draggable) || undefined}
onMouseenter={this.onMouseEnter}
onMouseleave={this.onMouseLeave}
onContexmenu={this.onContextMenu}
onClick={this.onSelectorClick}
onDragstart={this.onDragStart}
>
{$icon}{$title}
</span>
)
},
// Children list wrapped with `Animation`
renderChildren () {
const { expanded, pos } = this
const { rcTree: {
prefixCls,
openTransitionName, openAnimation,
renderTreeNode,
}} = this.context
// [Legacy] Animation control
const renderFirst = this.renderFirst
this.renderFirst = 1
let transitionAppear = true
if (!renderFirst && expanded) {
transitionAppear = false
}
const animProps = {}
if (openTransitionName) {
animProps.transitionName = openTransitionName
} else if (typeof openAnimation === 'object') {
animProps.animation = { ...openAnimation }
if (!transitionAppear) {
delete animProps.animation.appear
}
}
// Children TreeNode
const nodeList = this.getNodeChildren()
if (nodeList.length === 0) {
return null
}
let $children
if (expanded) {
$children = (
<ul
class={classNames(
`${prefixCls}-child-tree`,
expanded && `${prefixCls}-child-tree-open`,
)}
data-expanded={expanded}
>
{nodeList.map((node, index) => (
renderTreeNode(node, index, pos)
))}
</ul>
)
}
return (
<Animate
{...animProps}
showProp='data-expanded'
transitionAppear={transitionAppear}
component=''
>
{$children}
</Animate>
)
},
render () {
const {
dragOver, dragOverGapTop, dragOverGapBottom,
} = this
const { rcTree: {
prefixCls,
filterTreeNode,
}} = this.context
const disabled = this.isDisabled()
return (
<li
class={{
[`${prefixCls}-treenode-disabled`]: disabled,
'drag-over': !disabled && dragOver,
'drag-over-gap-top': !disabled && dragOverGapTop,
'drag-over-gap-bottom': !disabled && dragOverGapBottom,
'filter-node': filterTreeNode && filterTreeNode(this),
}}
onDragenter={this.onDragEnter}
onDragover={this.onDragOver}
onDragleave={this.onDragLeave}
onDrop={this.onDrop}
onDragend={this.onDragEnd}
>
{this.renderSwitcher()}
{this.renderCheckbox()}
{this.renderSelector()}
{this.renderChildren()}
</li>
)
},
}
TreeNode.isTreeNode = 1
export default TreeNode

@ -0,0 +1,6 @@
import Tree from './Tree'
import TreeNode from './TreeNode'
Tree.TreeNode = TreeNode
export { TreeNode }
export default Tree

@ -0,0 +1,399 @@
/* eslint no-loop-func: 0*/
import { Children } from 'react'
import warning from 'warning'
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('-')
}
// 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}`
}
export function getNodeChildren (children) {
const childList = Array.isArray(children) ? children : [children]
return childList
.filter(child => child && child.type && child.type.isTreeNode)
}
export function isCheckDisabled (node) {
const { disabled, disableCheckbox } = node.props || {}
return !!(disabled || disableCheckbox)
}
export function traverseTreeNodes (treeNodes, subTreeData, callback) {
if (typeof subTreeData === 'function') {
callback = subTreeData
subTreeData = false
}
function processNode (node, index, parent) {
const children = node ? node.props.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,
}
// Children data is not must have
if (subTreeData) {
// Statistic children
const subNodes = []
Children.forEach(childList, (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
}
}
// Process children node
Children.forEach(childList, (subNode, subIndex) => {
processNode(subNode, subIndex, { node, pos })
})
}
processNode(null)
}
/**
* [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 } = node.props
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 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) {
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) {
if (!keyList) {
return []
}
const { autoExpandParent, children } = props
// Do nothing if not auto expand parent
if (!autoExpandParent) {
return keyList
}
// 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]
if (isCheckDisabled(node)) return
tgtCheckedKeys[key] = true
// 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]),
}
}
/**
* Calculate the value of checked and halfChecked keys.
* This should be only run in init or props changed.
*/
export function calcCheckedKeys (keys, props) {
const { checkable, children, 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
}
// Do nothing if is checkStrictly mode
if (checkStrictly) {
return keyProps
}
// Conduct calculate the check status
const { checkedKeys = [] } = keyProps
return calcCheckStateConduct(children, checkedKeys)
}
Loading…
Cancel
Save