ant-design-vue/components/vc-tree/src/TreeNode.jsx

590 lines
15 KiB
React
Raw Normal View History

2018-03-23 13:30:20 +00:00
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'
2018-03-24 09:12:22 +00:00
import BaseMixin from '../../_util/BaseMixin'
import getTransitionProps from '../../_util/getTransitionProps'
2018-03-23 13:30:20 +00:00
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,
2018-03-24 09:12:22 +00:00
vcTreeNode: PropTypes.shape({
2018-03-23 13:30:20 +00:00
onUpCheckConduct: PropTypes.func,
}),
}
const TreeNode = {
2018-03-24 09:12:22 +00:00
name: 'TreeNode',
mixins: [BaseMixin],
2018-03-23 13:30:20 +00:00
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,
2018-03-24 09:12:22 +00:00
title: PropTypes.any,
2018-03-23 13:30:20 +00:00
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: {
2018-03-24 09:12:22 +00:00
vcTree: { default: {}},
2018-03-23 13:30:20 +00:00
},
2018-03-24 09:12:22 +00:00
provide () {
return {
vcTree: this.vcTree,
vcTreeNode: this,
}
2018-03-23 13:30:20 +00:00
},
// Isomorphic needn't load data in server side
mounted () {
2018-03-24 09:12:22 +00:00
this.syncLoadData(this.$props)
2018-03-23 13:30:20 +00:00
},
2018-03-24 09:12:22 +00:00
watch: {
expanded (val) {
this.syncLoadData({ expanded: val })
},
2018-03-23 13:30:20 +00:00
},
2018-03-24 09:12:22 +00:00
methods: {
onUpCheckConduct (treeNode, nodeChecked, nodeHalfChecked) {
const { pos: nodePos } = getOptionProps(treeNode)
const { eventKey, pos, checked, halfChecked } = this
const {
vcTree: { checkStrictly, isKeyChecked, onBatchNodeCheck, onCheckConductFinished },
vcTreeNode: { onUpCheckConduct } = {},
} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Stop conduct when current node is disabled
if (isCheckDisabled(this)) {
onCheckConductFinished()
2018-03-23 13:30:20 +00:00
return
}
2018-03-24 09:12:22 +00:00
const children = this.getNodeChildren()
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
let checkedCount = nodeChecked ? 1 : 0
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Statistic checked count
children.forEach((node, index) => {
const childPos = getPosition(pos, index)
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
if (nodePos === childPos || isCheckDisabled(node)) {
return
}
if (isKeyChecked(node.key || childPos)) {
checkedCount += 1
}
})
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// 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()
}
2018-03-23 13:30:20 +00:00
} else {
// Flush all the update
onCheckConductFinished()
}
2018-03-24 09:12:22 +00:00
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onDownCheckConduct (nodeChecked) {
const { $slots } = this
const children = $slots.default || []
const { vcTree: { checkStrictly, isKeyChecked, onBatchNodeCheck }} = this
if (checkStrictly) return
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
traverseTreeNodes(children, ({ node, key }) => {
if (isCheckDisabled(node)) return false
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
if (nodeChecked !== isKeyChecked(key)) {
onBatchNodeCheck(key, nodeChecked, false)
}
})
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onSelectorClick (e) {
if (this.isSelectable()) {
this.onSelect(e)
} else {
this.onCheck(e)
}
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onSelect (e) {
if (this.isDisabled()) return
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
const { vcTree: { onNodeSelect }} = this
e.preventDefault()
onNodeSelect(e, this)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onCheck (e) {
if (this.isDisabled()) return
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
const { disableCheckbox, checked, eventKey } = this
const {
vcTree: { checkable, onBatchNodeCheck, onCheckConductFinished },
vcTreeNode: { onUpCheckConduct } = {},
} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
if (!checkable || disableCheckbox) return
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
e.preventDefault()
const targetChecked = !checked
onBatchNodeCheck(eventKey, targetChecked, false, this)
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Children conduct
this.onDownCheckConduct(targetChecked)
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Parent conduct
if (onUpCheckConduct) {
onUpCheckConduct(this, targetChecked, false)
} else {
onCheckConductFinished()
}
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onMouseEnter (e) {
const { vcTree: { onNodeMouseEnter }} = this
onNodeMouseEnter(e, this)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onMouseLeave (e) {
const { vcTree: { onNodeMouseLeave }} = this
onNodeMouseLeave(e, this)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onContextMenu (e) {
const { vcTree: { onNodeContextMenu }} = this
onNodeContextMenu(e, this)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onDragStart (e) {
const { vcTree: { onNodeDragStart }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
e.stopPropagation()
this.setState({
dragNodeHighlight: true,
})
onNodeDragStart(e, this)
try {
// ie throw error
// firefox-need-it
e.dataTransfer.setData('text/plain', '')
} catch (error) {
// empty
}
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onDragEnter (e) {
const { vcTree: { onNodeDragEnter }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
e.preventDefault()
e.stopPropagation()
onNodeDragEnter(e, this)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onDragOver (e) {
const { vcTree: { onNodeDragOver }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
e.preventDefault()
e.stopPropagation()
onNodeDragOver(e, this)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onDragLeave (e) {
const { vcTree: { onNodeDragLeave }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
e.stopPropagation()
onNodeDragLeave(e, this)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onDragEnd (e) {
const { vcTree: { onNodeDragEnd }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
e.stopPropagation()
this.setState({
dragNodeHighlight: false,
})
onNodeDragEnd(e, this)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
onDrop (e) {
const { vcTree: { onNodeDrop }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
e.preventDefault()
e.stopPropagation()
this.setState({
dragNodeHighlight: false,
2018-03-23 13:30:20 +00:00
})
2018-03-24 09:12:22 +00:00
onNodeDrop(e, this)
},
// Disabled item still can be switch
onExpand (e) {
const { vcTree: { onNodeExpand }} = this
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 })
})
}
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Drag usage
setSelectHandle (node) {
this.selectHandle = node
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
getNodeChildren () {
const { $slots: { default: children }} = this
const originList = filterEmpty(children)
const targetList = getNodeChildren(originList)
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
if (originList.length !== targetList.length && !onlyTreeNodeWarned) {
onlyTreeNodeWarned = true
warning(false, 'Tree only accept TreeNode as children.')
}
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
return targetList
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
getNodeState () {
const { expanded } = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
if (this.isLeaf2()) {
return null
}
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
return expanded ? ICON_OPEN : ICON_CLOSE
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
isLeaf2 () {
const { isLeaf, loadStatus } = this
const { vcTree: { loadData }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
const hasChildren = this.getNodeChildren().length !== 0
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
return (
isLeaf ||
(!loadData && !hasChildren) ||
(loadData && loadStatus === LOAD_STATUS_LOADED && !hasChildren)
)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
isDisabled () {
const { disabled } = this
const { vcTree: { disabled: treeDisabled }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Follow the logic of Selectable
if (disabled === false) {
return false
}
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
return !!(treeDisabled || disabled)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
isSelectable () {
const { selectable } = this
const { vcTree: { selectable: treeSelectable }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Ignore when selectable is undefined or null
if (typeof selectable === 'boolean') {
return selectable
}
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
return treeSelectable
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Load data to avoid default expanded tree without data
syncLoadData (props) {
const { loadStatus } = this
const { expanded } = props
const { vcTree: { loadData }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
if (loadData && loadStatus === LOAD_STATUS_NONE && expanded && !this.isLeaf2()) {
this.setState({ loadStatus: LOAD_STATUS_LOADING })
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
loadData(this).then(() => {
this.setState({ loadStatus: LOAD_STATUS_LOADED })
}).catch(() => {
this.setState({ loadStatus: LOAD_STATUS_FAILED })
})
}
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Switcher
renderSwitcher () {
const { expanded } = this
const { vcTree: { prefixCls }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
if (this.isLeaf2()) {
return <span class={`${prefixCls}-switcher ${prefixCls}-switcher-noop`} />
}
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
return (
<span
class={classNames(
`${prefixCls}-switcher`,
`${prefixCls}-switcher_${expanded ? ICON_OPEN : ICON_CLOSE}`,
)}
onClick={this.onExpand}
/>
)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Checkbox
renderCheckbox () {
const { checked, halfChecked, disableCheckbox } = this
const { vcTree: { prefixCls, checkable }} = this
const disabled = this.isDisabled()
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
if (!checkable) return null
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// [Legacy] Custom element should be separate with `checkable` in future
const $custom = typeof checkable !== 'boolean' ? checkable : null
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
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>
)
},
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
renderIcon () {
const { loadStatus } = this
const { vcTree: { prefixCls }} = this
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
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 { vcTree: { prefixCls, showIcon, draggable, loadData }} = this
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()
}
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Title
const $title = <span class={`${prefixCls}-title`}>{title}</span>
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
return (
2018-03-23 13:30:20 +00:00
<span
2018-03-24 09:12:22 +00:00
ref='selectHandle'
title={typeof title === 'string' ? title : ''}
2018-03-23 13:30:20 +00:00
class={classNames(
2018-03-24 09:12:22 +00:00
`${wrapClass}`,
`${wrapClass}-${this.getNodeState() || 'normal'}`,
(!disabled && (selected || dragNodeHighlight)) && `${prefixCls}-node-selected`,
(!disabled && draggable) && 'draggable'
2018-03-23 13:30:20 +00:00
)}
2018-03-24 09:12:22 +00:00
draggable={(!disabled && draggable) || undefined}
aria-grabbed={(!disabled && draggable) || undefined}
onMouseenter={this.onMouseEnter}
onMouseleave={this.onMouseLeave}
onContexmenu={this.onContextMenu}
onClick={this.onSelectorClick}
onDragstart={this.onDragStart}
2018-03-23 13:30:20 +00:00
>
2018-03-24 09:12:22 +00:00
{$icon}{$title}
2018-03-23 13:30:20 +00:00
</span>
2018-03-24 09:12:22 +00:00
)
},
// Children list wrapped with `Animation`
renderChildren () {
const { expanded, pos } = this
const { vcTree: {
prefixCls,
openTransitionName, openAnimation,
renderTreeNode,
}} = this
// [Legacy] Animation control
const renderFirst = this.renderFirst
this.renderFirst = 1
let transitionAppear = true
if (!renderFirst && expanded) {
transitionAppear = false
}
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
let animProps = {}
if (openTransitionName) {
animProps = getTransitionProps(openTransitionName, { appear: transitionAppear })
} else if (typeof openAnimation === 'object') {
animProps = { ...openAnimation }
if (!transitionAppear) {
delete animProps.props.appear
}
}
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
// Children TreeNode
const nodeList = this.getNodeChildren()
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
if (nodeList.length === 0) {
return null
2018-03-23 13:30:20 +00:00
}
2018-03-24 09:12:22 +00:00
let $children
if (expanded) {
$children = (
<ul
v-show={expanded}
class={classNames(
`${prefixCls}-child-tree`,
expanded && `${prefixCls}-child-tree-open`,
)}
data-expanded={expanded}
>
{nodeList.map((node, index) => (
renderTreeNode(node, index, pos)
))}
</ul>
)
}
2018-03-23 13:30:20 +00:00
2018-03-24 09:12:22 +00:00
return (
<transition
{...animProps}
2018-03-23 13:30:20 +00:00
>
2018-03-24 09:12:22 +00:00
{$children}
</transition>
2018-03-23 13:30:20 +00:00
)
2018-03-24 09:12:22 +00:00
},
2018-03-23 13:30:20 +00:00
},
render () {
const {
dragOver, dragOverGapTop, dragOverGapBottom,
} = this
2018-03-24 09:12:22 +00:00
const { vcTree: {
2018-03-23 13:30:20 +00:00
prefixCls,
filterTreeNode,
2018-03-24 09:12:22 +00:00
}} = this
2018-03-23 13:30:20 +00:00
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