Skip to content

Commit 990694c

Browse files
committed
feat: support expand animation
1 parent f7983d9 commit 990694c

File tree

6 files changed

+194
-10
lines changed

6 files changed

+194
-10
lines changed

examples/Feature.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
ref="basicTreeRef"
1111
:data="basicUsage"
1212
:nodeClassName="node => `generated-class-${node.id}`"
13+
animation
1314
@click="handleClick"
1415
@node-dblclick="handleDblClick"
1516
@node-right-click="handleRightClick"
@@ -145,10 +146,10 @@
145146
<div class="interface">
146147
<div style="height: 300px">
147148
<VTree
148-
ref="basicTreeRef"
149149
:data="showLineTreeData"
150150
:showLine="{ type: showLineType, polyline: showLinePolyline }"
151151
defaultExpandAll
152+
animation
152153
/>
153154
</div>
154155
</div>

src/components/Tree.vue

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,14 @@
1010
<div :class="blockAreaCls">
1111
<div :style="{ height: `${topSpaceHeight}px` }"></div>
1212
<VTreeNode
13-
v-for="node in renderNodes"
13+
v-for="node in (expandAnimation.ready.value ? expandAnimation.topNodes.value : renderNodes)"
1414
v-bind="treeNodeProps"
1515
:key="node[keyField]"
1616
:data="node"
1717
:getNode="getNode"
1818
:noSiblingNodeMap="noSiblingNodeMap"
1919
v-on="treeNodeListeners"
20-
:class="
21-
typeof nodeClassName === 'function'
22-
? nodeClassName(node)
23-
: nodeClassName
24-
"
20+
:class="getNodeClassName(node)"
2521
:style="{
2622
minHeight: `${nodeMinHeight}px`,
2723
}"
@@ -30,6 +26,55 @@
3026
@expand="handleNodeExpand"
3127
@node-drop="handleNodeDrop"
3228
/>
29+
<template v-if="expandAnimation.ready.value">
30+
<Transition
31+
name="vtree-expand-animation"
32+
@after-enter="expandAnimation.onExpandAnimationFinish"
33+
@after-leave="expandAnimation.onExpandAnimationFinish"
34+
>
35+
<div
36+
v-show="expandAnimation.currentExpandState.value"
37+
:style="{
38+
display: 'grid',
39+
}"
40+
>
41+
<div :style="{ overflow: 'hidden' }">
42+
<VTreeNode
43+
v-for="node in expandAnimation.middleNodes.value"
44+
v-bind="treeNodeProps"
45+
:key="node[keyField]"
46+
:data="node"
47+
:getNode="getNode"
48+
:noSiblingNodeMap="noSiblingNodeMap"
49+
:class="getNodeClassName(node)"
50+
:style="{
51+
minHeight: `${nodeMinHeight}px`,
52+
}"
53+
@check="handleNodeCheck"
54+
@select="handleNodeSelect"
55+
@expand="handleNodeExpand"
56+
@node-drop="handleNodeDrop"
57+
/>
58+
</div>
59+
</div>
60+
</Transition>
61+
<VTreeNode
62+
v-for="node in expandAnimation.bottomNodes.value"
63+
v-bind="treeNodeProps"
64+
:key="node[keyField]"
65+
:data="node"
66+
:getNode="getNode"
67+
:noSiblingNodeMap="noSiblingNodeMap"
68+
:class="getNodeClassName(node)"
69+
:style="{
70+
minHeight: `${nodeMinHeight}px`,
71+
}"
72+
@check="handleNodeCheck"
73+
@select="handleNodeSelect"
74+
@expand="handleNodeExpand"
75+
@node-drop="handleNodeDrop"
76+
/>
77+
</template>
3378
<div :style="{ height: `${bottomSpaceHeight}px` }"></div>
3479
</div>
3580
</div>
@@ -153,6 +198,9 @@ export interface TreeProps {
153198
/** 连接线 */
154199
showLine?: boolean | ShowLine
155200
201+
/** 是否启用过渡动画,目前仅控制展开收起 */
202+
animation?: boolean
203+
156204
/** 子节点缩进 */
157205
nodeIndent?: number,
158206
@@ -225,6 +273,7 @@ import { useIframeResize } from '../hooks/useIframeResize'
225273
import { usePublicTreeAPI } from '../hooks/usePublicTreeAPI'
226274
import { FilterFunctionType } from '../store/tree-store'
227275
import { pickReadonly } from '../utils'
276+
import { useExpandAnimation } from '../hooks/useExpandAnimation'
228277
229278
const props = withDefaults(defineProps<TreeProps>(), DEFAULT_TREE_PROPS)
230279
@@ -306,6 +355,8 @@ const {
306355
scrollTo,
307356
} = useVirtualList(nonReactive, props)
308357
358+
const expandAnimation = useExpandAnimation(renderNodes, props)
359+
309360
const {
310361
unloadCheckedNodes,
311362
isRootLoading,
@@ -347,6 +398,10 @@ const {
347398
updateRender,
348399
})
349400
401+
const getNodeClassName = (node: TreeNode) => {
402+
return typeof props.nodeClassName === 'function' ? props.nodeClassName(node) : props.nodeClassName
403+
}
404+
350405
const noSiblingNodeMap = computed(() => {
351406
const parentsOfFirstNode: TreeNode[] = []
352407
let nodeParent = renderNodes.value[0]?._parent
@@ -421,6 +476,7 @@ const handleNodeSelect = (node: TreeNode): void => {
421476
nonReactive.store.setSelected(node[props.keyField], !node.selected)
422477
}
423478
const handleNodeExpand = (node: TreeNode): void => {
479+
expandAnimation.updateBeforeExpand(node)
424480
nonReactive.store.setExpand(node[props.keyField], !node.expand)
425481
}
426482
const handleNodeDrop = (
@@ -553,6 +609,7 @@ const emitSelectableInput = (
553609
}
554610
555611
onMounted(() => {
612+
nonReactive.store.on('expand', expandAnimation.updateAfterExpand)
556613
nonReactive.store.on('visible-data-change', updateBlockNodes)
557614
nonReactive.store.on('render-data-change', updateRender)
558615
nonReactive.store.on(

src/components/TreeDrop.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
</div>
2121

2222
<!-- 下拉框 -->
23-
<transition name="vtree-dropdown">
23+
<Transition name="vtree-dropdown">
2424
<div
2525
ref="dropdownRef"
2626
v-show="dropdownVisible"
@@ -43,7 +43,7 @@
4343
</template>
4444
</VTreeSearch>
4545
</div>
46-
</transition>
46+
</Transition>
4747
</div>
4848
</template>
4949

src/hooks/useExpandAnimation.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { Ref, nextTick, ref } from "vue"
2+
import { TreeNode } from "../store"
3+
import { TreeProps } from "../components/Tree.vue"
4+
5+
type IUseExpandAnimationProps = Required<Pick<TreeProps,
6+
'keyField' |
7+
'animation'
8+
>>
9+
10+
export const useExpandAnimation = (renderNodesRef: Ref<TreeNode[]>, props: IUseExpandAnimationProps) => {
11+
const expandAnimationStart = ref(false)
12+
const expandAnimationReady = ref(false)
13+
const expandNodeIndex = ref(-1)
14+
const expandNodeLevel = ref(-1)
15+
const expandNodeCurrentState = ref(false)
16+
const expandNodeNextState = ref(false)
17+
18+
const expandTopNodes = ref<TreeNode[]>([])
19+
const expandMiddleNodes = ref<TreeNode[]>([])
20+
const expandBottomNodes = ref<TreeNode[]>([])
21+
22+
const resetExpandAnimation = () => {
23+
expandAnimationStart.value = false
24+
expandAnimationReady.value = false
25+
expandNodeIndex.value = -1
26+
expandNodeLevel.value = -1
27+
28+
expandTopNodes.value = []
29+
expandMiddleNodes.value = []
30+
expandBottomNodes.value = []
31+
}
32+
33+
const updateMiddleNodes = () => {
34+
const nodeToExpandLevel = expandNodeLevel.value
35+
const middleNodes: TreeNode[] = []
36+
const renderNodesLength = renderNodesRef.value.length
37+
for (let i = expandNodeIndex.value + 1; i < renderNodesLength; i++) {
38+
if (renderNodesRef.value[i]._level > nodeToExpandLevel) {
39+
middleNodes.push(renderNodesRef.value[i])
40+
} else break
41+
}
42+
expandMiddleNodes.value = middleNodes
43+
}
44+
45+
const updateBeforeExpand = (nodeToExpand: TreeNode) => {
46+
if (!props.animation) return
47+
resetExpandAnimation()
48+
49+
const key = nodeToExpand[props.keyField]
50+
const index = renderNodesRef.value.findIndex((renderNode) => renderNode[props.keyField] === key)
51+
if (index > -1) {
52+
expandNodeIndex.value = index
53+
expandNodeLevel.value = nodeToExpand._level
54+
expandAnimationStart.value = true
55+
expandNodeCurrentState.value = nodeToExpand.expand
56+
expandNodeNextState.value = !nodeToExpand.expand
57+
58+
if (expandNodeNextState.value) {
59+
expandBottomNodes.value = renderNodesRef.value.slice(expandNodeIndex.value + 1)
60+
} else {
61+
updateMiddleNodes()
62+
}
63+
}
64+
}
65+
66+
const updateAfterExpand = () => {
67+
if (!props.animation) return
68+
69+
if (!expandAnimationStart.value) {
70+
expandAnimationStart.value = false
71+
return
72+
}
73+
74+
if (expandNodeIndex.value === -1) return
75+
76+
nextTick(() => {
77+
expandTopNodes.value = renderNodesRef.value.slice(0, expandNodeIndex.value + 1)
78+
if (expandNodeNextState.value) {
79+
updateMiddleNodes()
80+
} else {
81+
expandBottomNodes.value = renderNodesRef.value.slice(expandNodeIndex.value + 1)
82+
}
83+
expandAnimationReady.value = true
84+
nextTick(() => {
85+
expandNodeCurrentState.value = !expandNodeCurrentState.value
86+
})
87+
})
88+
}
89+
90+
const onExpandAnimationFinish = () => {
91+
resetExpandAnimation()
92+
}
93+
94+
return {
95+
ready: expandAnimationReady,
96+
currentExpandState: expandNodeCurrentState,
97+
98+
topNodes: expandTopNodes,
99+
middleNodes: expandMiddleNodes,
100+
bottomNodes: expandBottomNodes,
101+
102+
updateBeforeExpand,
103+
updateAfterExpand,
104+
onExpandAnimationFinish,
105+
}
106+
}

src/styles/tree-drop.less

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@
145145

146146
// Dropdown animation
147147
.@{tree-dropdown-prefix} {
148-
&-enter,
148+
&-enter-from,
149149
&-leave-to {
150150
opacity: 0;
151151
transform: scaleY(0.8);

src/styles/tree.less

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// prefix
33
@tree-prefix: ~'@{vtree-prefix}-tree';
44
@tree-node-prefix: ~'@{tree-prefix}-node';
5+
@tree-expand-animation: ~'@{vtree-prefix}-expand-animation';
56

67
// Tree
78
.@{tree-prefix} {
@@ -304,6 +305,25 @@
304305
}
305306
}
306307
}
308+
309+
.@{tree-expand-animation} {
310+
&-enter-from,
311+
&-leave-to {
312+
opacity: 0;
313+
grid-template-rows: 0fr;
314+
}
315+
316+
&-enter-to,
317+
&-leave-from {
318+
opacity: 1;
319+
grid-template-rows: 1fr;
320+
}
321+
322+
&-enter-active,
323+
&-leave-active {
324+
transition: opacity .2s, grid-template-rows .2s;
325+
}
326+
}
307327
}
308328

309329
#vtree-tree-styles();

0 commit comments

Comments
 (0)