refactor(tree): wip

This commit is contained in:
07akioni 2020-02-25 18:47:41 +08:00
parent a4cc14c9ae
commit 1902e394fc
10 changed files with 254 additions and 196 deletions

View File

@ -1,51 +0,0 @@
export default {
props: {
transitionDisabled: {
type: Boolean,
default: false
}
},
methods: {
handleBeforeLeave () {
this.$el.style.maxHeight = this.$el.offsetHeight + 'px'
this.$el.style.height = this.$el.offsetHeight + 'px'
this.$el.getBoundingClientRect()
},
handleLeave () {
// debugger
this.$el.style.maxHeight = 0
this.$el.getBoundingClientRect()
},
handleEnter () {
this.$nextTick().then(() => {
this.$el.style.height = this.$el.offsetHeight + 'px'
this.$el.style.maxHeight = 0
this.$el.getBoundingClientRect()
this.$el.style.maxHeight = this.$el.style.height
})
},
handleAfterEnter () {
this.$el.style.height = null
this.$el.style.maxHeight = null
}
},
beforeDestroy () {
if (this.transitionDisabled) {
const parent = this.$el.parentElement
if (parent) parent.removeChild(this.$el)
}
},
render (h) {
return h('transition', {
props: {
name: 'n-fade-in-height-expand-transition'
},
on: {
beforeLeave: this.handleBeforeLeave,
leave: this.handleLeave,
enter: this.handleEnter,
afterEnter: this.handleAfterEnter
}
}, this.$slots.default)
}
}

View File

@ -1,53 +1,46 @@
import {
treedOptions,
dropIsValid,
applyDrop,
linkedCascaderOptions,
menuOptions
} from '../../_utils/data/menuModel'
import withapp from '../../_mixins/withapp' import withapp from '../../_mixins/withapp'
import themeable from '../../_mixins/themeable' import themeable from '../../_mixins/themeable'
import NTreeNode from './TreeNode' import NTreeNode from './TreeNode'
import NTreeChildNodesExpandTransition from './ChildNodesExpandTransition' import NFadeInHeightExpandTransition from '../../_transition/FadeInHeightExpandTransition'
import { isLeaf, isLoaded } from './utils'
function createNode (node, h, self) { function createNode (node, h, treeInstance) {
const listeners = { const listeners = {
'switcher-click': self.handleSwitcherClick, 'switcher-click': treeInstance.handleSwitcherClick,
select: self.handleSelect, select: treeInstance.handleSelect,
dragenter: self.handleDragEnter, dragenter: treeInstance.handleDragEnter,
dragstart: self.handleDragStart, dragstart: treeInstance.handleDragStart,
dragleave: self.handleDragLeave, dragleave: treeInstance.handleDragLeave,
drop: self.handleDrop, drop: treeInstance.handleDrop,
check: self.handleCheck check: treeInstance.handleCheck
} }
const expanded = self.syntheticExpandedKeys.includes(node.key) const expanded = treeInstance.syntheticExpandedKeys.includes(node.key)
const props = { const props = {
data: node, data: node,
expanded, expanded,
selected: self.syntheticSelectedKeys.includes(node.key), selected: treeInstance.syntheticSelectedKeys.includes(node.key),
draggable: self.draggable, draggable: treeInstance.draggable,
checkable: self.checkable, checkable: treeInstance.checkable,
drop: self.drop, drop: treeInstance.drop,
blockNode: self.blockNode, blockNode: treeInstance.blockNode,
checked: self.syntheticCheckedKeys.includes(node.key) checked: treeInstance.syntheticCheckedKeys.includes(node.key)
} }
return h(NTreeNode, { return h(NTreeNode, {
props, props,
on: listeners, on: listeners,
key: node.key key: node.key
}, },
[!node.isLeaf [!isLeaf(node)
? h(NTreeChildNodesExpandTransition, { ? h(NFadeInHeightExpandTransition, {
props: { props: {
transitionDisabled: self.transitionDisabled transitionDisabled: treeInstance.transitionDisabled
} }
}, },
[ [
expanded expanded && node.children
? h('ul', { ? h('ul', {
staticClass: 'n-tree-children-wrapper' staticClass: 'n-tree-children-wrapper'
}, node.children.map(child => createNode(child, h, self))) }, node.children.map(child => createNode(child, h, treeInstance)))
: null : null
] ]
) )
@ -55,25 +48,36 @@ function createNode (node, h, self) {
) )
} }
function convertRootedOptionsToVNodeTree (root, h, self) { function convertOptionsToVNodeTree (options, h, treeInstance) {
return root.children.map(child => createNode(child, h, self)) return options.map(child => createNode(child, h, treeInstance))
} }
export default { export default {
name: 'NTree', name: 'NTree',
mixins: [ withapp, themeable ], mixins: [ withapp, themeable ],
model: {
prop: 'selected-keys',
event: 'selected-keys-change'
},
provide () {
return { NTree: this }
},
props: { props: {
data: { data: {
type: Array, type: Array,
default: null default: null
}, },
autoExpandParent: {
type: Boolean,
default: true
},
checkable: { checkable: {
type: Boolean, type: Boolean,
default: false default: false
}, },
draggable: { draggable: {
type: Boolean, type: Boolean,
default: true default: false
}, },
blockNode: { blockNode: {
type: Boolean, type: Boolean,
@ -83,6 +87,10 @@ export default {
type: Array, type: Array,
default: null default: null
}, },
disabled: {
type: Boolean,
default: false
},
defaultCheckedKeys: { defaultCheckedKeys: {
type: Array, type: Array,
default: null default: null
@ -99,18 +107,10 @@ export default {
type: Array, type: Array,
default: null default: null
}, },
lazy: { remote: {
type: Boolean, type: Boolean,
default: false default: false
}, },
allowDrop: {
type: Function,
default: () => true
},
allowDrag: {
type: Function,
default: () => true
},
multiple: { multiple: {
type: Boolean, type: Boolean,
default: false default: false
@ -119,36 +119,15 @@ export default {
type: String, type: String,
default: '' default: ''
}, },
onExpand: { onLoad: {
type: Function, type: Function,
default: () => { default: null
return (node, next) => {
next()
}
}
},
onSelect: {
type: Function,
default: () => {
return (node, next) => {
next()
}
}
},
onDrop: {
type: Function,
default: () => {
return (node, next) => {
next()
}
}
} }
}, },
created () { created () {
this.internalCheckedKeys = this.defaultCheckedKeys || this.internalCheckedKeys this.internalCheckedKeys = this.defaultCheckedKeys || []
this.internalExpandedKeys = this.defaultExpandedKeys || this.internalExpandedKeys this.internalExpandedKeys = this.defaultExpandedKeys || []
this.internalSelectedKeys = this.defaultSelectedKeys || this.internalSelectedKeys this.internalSelectedKeys = this.defaultSelectedKeys || []
this.treeData = treedOptions(this.data)
}, },
data () { data () {
return { return {
@ -160,18 +139,16 @@ export default {
draggingNode: null, draggingNode: null,
droppingNodeKey: null, droppingNodeKey: null,
expandTimerId: null, expandTimerId: null,
transitionDisabled: false transitionDisabled: false,
loadingKeys: []
} }
}, },
watch: { watch: {
data (newData) { data () {
this.treeData = treedOptions(newData)
this.internalExpandedKeys = [] this.internalExpandedKeys = []
this.internalCheckedKeys = [] this.internalCheckedKeys = []
this.internalSelectedKeys = [] this.internalSelectedKeys = []
this.draggingNodeKey = null this.loadingKeys = []
this.draggingNode = null
this.droppingNodeKey = null
this.expandTimerId = null this.expandTimerId = null
} }
}, },
@ -208,25 +185,33 @@ export default {
} }
}, },
methods: { methods: {
getSelectedKeys () {
return this.syntheticSelectedKeys
},
getCheckedKeys () {
return this.syntheticCheckedKeys
},
getExpandedKeys () {
return this.syntheticExpandedKeys
},
disableTransition () { disableTransition () {
this.transitionDisabled = true this.transitionDisabled = true
}, },
enableTransition () { enableTransition () {
this.transitionDisabled = false this.transitionDisabled = false
}, },
resetDragStatus () {
this.draggingNodeKey = null
this.draggingNode = null
this.droppingNodeKey = null
},
handleCheck (node, checked) { handleCheck (node, checked) {
if (!this.hasCheckedKeys) { if (this.disabled || node.disabled) return
if (checked) { if (checked) {
if (this.hasCheckedKeys) {
this.$emit('checked-keys-change', this.syntheticCheckedKeys.concat([node.key]))
} else {
this.internalCheckedKeys.push(node.key) this.internalCheckedKeys.push(node.key)
}
} else {
if (this.hasCheckedKeys) {
const checkedKeysAfterChange = this.syntheticCheckedKeys
checkedKeysAfterChange.splice(
checkedKeysAfterChange.findIndex(key => key === node.key),
1
)
this.$emit('checked-keys-change', checkedKeysAfterChange)
} else { } else {
this.internalCheckedKeys.splice( this.internalCheckedKeys.splice(
this.internalCheckedKeys.findIndex(key => key === node.key), this.internalCheckedKeys.findIndex(key => key === node.key),
@ -235,79 +220,121 @@ export default {
} }
} }
}, },
handleDrop (node, dropType) {
const drop = [this.draggingNode, node, dropType]
if (dropIsValid(drop)) {
this.disableTransition()
this.$nextTick().then(() => {
applyDrop(drop)
return this.$nextTick()
}).then(() => {
this.enableTransition()
})
}
this.draggingNodeKey = null
this.draggingNode = null
},
toggleExpand (node) { toggleExpand (node) {
const index = this.syntheticExpandedKeys.findIndex(expandNodeId => expandNodeId === node.key) if (this.disabled) return
const index = this.syntheticExpandedKeys
.findIndex(expandNodeId => expandNodeId === node.key)
if (~index) { if (~index) {
this.$emit('collapse', node)
if (!this.hasExpandedKeys) { if (!this.hasExpandedKeys) {
this.internalExpandedKeys.splice(index, 1) this.internalExpandedKeys.splice(index, 1)
this.$emit('expanded-keys-change', this.internalExpandedKeys)
} else {
const expandedKeysAfterChange = Array.from(this.syntheticExpandedKeys)
expandedKeysAfterChange.splice(index, 1)
this.$emit(
'expanded-keys-change',
expandedKeysAfterChange
)
} }
} else { } else {
if (!node.isLeaf) { if (!isLeaf(node)) {
this.$emit('expand', node) this.$emit('expand', node)
if (!this.hasExpandedKeys) { if (!this.hasExpandedKeys) {
this.internalExpandedKeys.push(node.key) this.internalExpandedKeys.push(node.key)
this.$emit('expanded-keys-change', this.internalExpandedKeys)
} else {
this.$emit(
'expanded-keys-change',
this.syntheticExpandedKeys.concat(node.key)
)
} }
} }
} }
}, },
handleSwitcherClick (node) { handleSwitcherClick (node) {
if (this.disabled || node.disabled) return
this.toggleExpand(node) this.toggleExpand(node)
}, },
handleSelect (node) { handleSelect (node) {
if (this.disabled || node.disabled) return
this.$emit('select', node) this.$emit('select', node)
if (this.internalSelectedKeys.includes(node.key)) this.internalSelectedKeys = [] if (this.internalSelectedKeys.includes(node.key)) this.internalSelectedKeys = []
else this.internalSelectedKeys = [node.key] else this.internalSelectedKeys = [node.key]
}, },
handleDragEnter (node) { handleDragEnter ({ event, node }) {
this.$emit('dragenter', node) if (!this.draggable || this.disabled || node.disabled) return
this.$emit('dragenter', { event, node })
if (!this.autoExpandParent) return
this.droppingNodeKey = node.key this.droppingNodeKey = node.key
if (node.key === this.draggingNodeKey) return if (node.key === this.draggingNodeKey) return
if (!this.syntheticExpandedKeys.includes(node.key) && !node.isLeaf) { if (
!this.syntheticExpandedKeys.includes(node.key) &&
!isLeaf(node)
) {
window.clearTimeout(this.expandTimerId) window.clearTimeout(this.expandTimerId)
this.expandTimerId = window.setTimeout(() => { const expand = () => {
if (this.droppingNodeKey === node.key && !this.syntheticExpandedKeys.includes(node.key)) { if (
this.droppingNodeKey === node.key &&
!this.syntheticExpandedKeys.includes(node.key)
) {
if (!this.hasExpandedKeys) { if (!this.hasExpandedKeys) {
this.internalExpandedKeys.push(node.key) this.internalExpandedKeys.push(node.key)
this.$emit('expanded-keys-change', this.internalExpandedKeys)
} else {
this.$emit('expanded-keys-change', this.syntheticExpandedKeys.concat(node.key))
} }
this.$emit('expand', node.key)
} }
}
if (!isLoaded(node)) {
if (!this.loadingKeys.includes(node.key)) {
this.loadingKeys.push(node.key)
}
this
.onLoad(node)
.then(() => {
this.loadingKeys.splice(
this.loadingKeys.find(key => key === node.key),
1
)
expand()
})
return
}
this.expandTimerId = window.setTimeout(() => {
expand()
this.expandTimerId = null this.expandTimerId = null
}, 800) }, 800)
} }
}, },
handleDragLeave (node) { handleDragLeave ({ event, node }) {
if (!this.draggable || this.disabled || node.disabled) return
this.droppingNodeKey = null this.droppingNodeKey = null
this.$emit('dragleave', node) this.$emit('dragleave', { event, node })
}, },
handleDragStart (node) { handleDragStart ({ event, node }) {
if (!this.draggable || this.disabled || node.disabled) return
this.draggingNodeKey = node.key this.draggingNodeKey = node.key
this.draggingNode = node this.draggingNode = node
this.$emit('dragstart', node) this.$emit('dragstart', { event, node })
},
handleDrop ({ event, node, dropPosition }) {
if (!this.draggable || this.disabled || node.disabled) return
const drop = {
event,
node,
dragNode: this.draggingNode,
dropPosition
}
this.$emit('drop', drop)
this.resetDragStatus()
} }
}, },
render (h) { render (h) {
const lOptions = linkedCascaderOptions(this.treeData, 'multiple-all-options')
const mOptions = menuOptions(lOptions)[0]
return h('div', { return h('div', {
staticClass: 'n-tree', staticClass: 'n-tree',
class: { class: {
[`n-${this.syntheticTheme}-theme`]: this.syntheticTheme [`n-${this.syntheticTheme}-theme`]: this.syntheticTheme
} }
}, convertRootedOptionsToVNodeTree(mOptions, h, this)) }, convertOptionsToVNodeTree(this.data, h, this))
} }
} }

View File

@ -1,9 +1,15 @@
import NTreeNodeSwitcher from './TreeNodeSwitcher.vue' import NTreeNodeSwitcher from './TreeNodeSwitcher.vue'
import NTreeNodeCheckbox from './TreeNodeCheckbox.vue' import NTreeNodeCheckbox from './TreeNodeCheckbox.vue'
import NTreeNodeContent from './TreeNodeContent.vue' import NTreeNodeContent from './TreeNodeContent.vue'
import { isLeaf, isLoaded } from './utils'
export default { export default {
name: 'NTreeNode', name: 'NTreeNode',
inject: {
NTree: {
default: null
}
},
props: { props: {
data: { data: {
type: Object, type: Object,
@ -38,27 +44,58 @@ export default {
default: null default: null
} }
}, },
computed: {
loading () {
return this.NTree.loadingKeys.includes(this.data.key)
}
},
methods: { methods: {
handleSwitcherClick () { handleSwitcherClick () {
this.$emit('switcher-click', this.data) const node = this.data
const NTree = this.NTree
if (NTree.remote && !isLeaf(node) && !isLoaded(node)) {
if (!NTree.loadingKeys.includes(node.key)) {
NTree.loadingKeys.push(node.key)
}
NTree.onLoad &&
NTree.onLoad(node)
.then(() => {
NTree.loadingKeys.splice(
NTree.loadingKeys.find(key => key === node.key),
1
)
this.$emit('switcher-click', node)
})
} else {
this.$emit('switcher-click', node)
}
}, },
handleContentClick () { handleContentClick () {
this.$emit('select', this.data) this.$emit('select', this.data)
}, },
handleDragOver () { handleDragOver (e) {
this.$emit('dragover', this.data) this.$emit('dragover', { event: e, node: this.data })
}, },
handleDragEnter () { handleDragEnter (e) {
this.$emit('dragenter', this.data) this.$emit('dragenter', { event: e, node: this.data })
}, },
handleDragStart () { handleDragStart (e) {
this.$emit('dragstart', this.data) this.$emit('dragstart', { event: e, node: this.data })
}, },
handleDragLeave () { handleDragLeave (e) {
this.$emit('dragleave', this.data) this.$emit('dragleave', { event: e, node: this.data })
},
handleDragEnd (e) {
this.$emit('dragend', { event: e, node: this.data })
this.resetDragStatus()
}, },
handleDrop (e, dropPosition) { handleDrop (e, dropPosition) {
this.$emit('drop', this.data, dropPosition) this.$emit('drop', {
event: e,
node: this.data,
dropPosition
})
this.NTree.resetDragStatus()
}, },
handleCheck (checked) { handleCheck (checked) {
this.$emit('check', this.data, checked) this.$emit('check', this.data, checked)
@ -71,7 +108,8 @@ export default {
h(NTreeNodeSwitcher, { h(NTreeNodeSwitcher, {
props: { props: {
expanded: this.expanded, expanded: this.expanded,
hide: this.data.isLeaf loading: this.loading,
hide: isLeaf(this.data)
}, },
on: { on: {
click: this.handleSwitcherClick click: this.handleSwitcherClick

View File

@ -2,7 +2,7 @@
<span class="n-tree-node-checkbox"> <span class="n-tree-node-checkbox">
<n-checkbox <n-checkbox
:checked="value" :checked="value"
@input="handleInput" @change="handleChange"
/> />
</span> </span>
</template> </template>
@ -22,7 +22,7 @@ export default {
} }
}, },
methods: { methods: {
handleInput (value) { handleChange (value) {
this.$emit('check', value) this.$emit('check', value)
} }
} }

View File

@ -88,12 +88,12 @@ export default {
handleContentDrop (e) { handleContentDrop (e) {
e.preventDefault() e.preventDefault()
this.pending = false this.pending = false
const actionType = ({ const dropPosition = ({
top: 'insertBefore', top: 'top',
bottom: 'insertAfter', bottom: 'bottom',
body: 'append' body: 'center'
})[this.pendingPosition] })[this.pendingPosition]
this.$emit('drop', e, actionType) this.$emit('drop', e, dropPosition)
} }
} }
} }

View File

@ -7,19 +7,33 @@
}" }"
@click="handleClick" @click="handleClick"
> >
<n-icon> <div class="n-tree-node-switcher__icon">
<n-icon-switch-transition>
<n-icon v-if="!loading" key="switcher">
<md-arrow-dropright /> <md-arrow-dropright />
</n-icon> </n-icon>
<n-base-loading v-else key="loading" :theme="NTree.syntheticTheme" />
</n-icon-switch-transition>
</div>
</span> </span>
</template> </template>
<script> <script>
import mdArrowDropright from '../../_icons/md-arrow-dropright' import mdArrowDropright from '../../_icons/md-arrow-dropright'
import NBaseLoading from '../../_base/Loading'
import NIconSwitchTransition from '../../_transition/IconSwitchTransition'
export default { export default {
name: 'NTreeSwitcher', name: 'NTreeSwitcher',
inject: {
NTree: {
default: null
}
},
components: { components: {
mdArrowDropright mdArrowDropright,
NBaseLoading,
NIconSwitchTransition
}, },
props: { props: {
expanded: { expanded: {
@ -29,6 +43,10 @@ export default {
hide: { hide: {
type: Boolean, type: Boolean,
default: false default: false
},
loading: {
type: Boolean,
default: false
} }
}, },
methods: { methods: {

8
src/Tree/src/utils.js Normal file
View File

@ -0,0 +1,8 @@
export function isLeaf (node) {
if (node.isLeaf !== undefined) return node.isLeaf
return !node.children
}
export function isLoaded (node) {
return !(node.isLeaf === false && !node.children)
}

View File

@ -32,9 +32,25 @@
justify-content: center; justify-content: center;
transition: transform .15s $--n-ease-in-out-cubic-bezier; transition: transform .15s $--n-ease-in-out-cubic-bezier;
vertical-align: bottom; vertical-align: bottom;
@include e(icon) {
position: relative;
height: 14px;
width: 14px;
display: flex;
@include b(icon) { @include b(icon) {
fill: $--n-secondary-text-color; height: 14px;
stroke: $--n-secondary-text-color; width: 14px;
font-size: 14px;
fill: $--tree-node-switcher-color;
stroke: $--tree-node-switcher-color;
@include icon-switch-transition;
}
@include b(base-loading) {
font-size: 14px;
height: 14px;
width: 14px;
@include icon-switch-transition;
}
} }
@include m(hide) { @include m(hide) {
visibility: hidden visibility: hidden

View File

@ -1,7 +1,8 @@
@mixin setup-dark-tree { @mixin setup-dark-tree {
$--tree-node-background-color: ( $--tree-node-background-color: (
'hover': change-color($--n-primary-color, $alpha: .45), 'hover': change-color($--n-primary-color, $alpha: .25),
'active': change-color($--n-primary-color, $alpha: .3), 'active': change-color($--n-primary-color, $alpha: .15),
'selected': change-color($--n-primary-color, $alpha: .3) 'selected': change-color($--n-primary-color, $alpha: .15)
) !global; ) !global;
$--tree-node-switcher-color: $--n-tertiary-text-color !global;
} }

View File

@ -1,7 +1,8 @@
@mixin setup-light-tree { @mixin setup-light-tree {
$--tree-node-background-color: ( $--tree-node-background-color: (
'hover': change-color($--n-primary-color, $alpha: .15), 'hover': change-color($--n-primary-color, $alpha: .16),
'active': change-color($--n-primary-color, $alpha: .25), 'active': change-color($--n-primary-color, $alpha: .1),
'selected': change-color($--n-primary-color, $alpha: .25) 'selected': change-color($--n-primary-color, $alpha: .1)
) !global; ) !global;
$--tree-node-switcher-color: $--n-tertiary-text-color !global;
} }