vue3-yuanma/packages/runtime-core/src/components/Transition.ts

200 lines
5.3 KiB
TypeScript
Raw Normal View History

import { createComponent } from '../apiCreateComponent'
import { getCurrentInstance } from '../component'
import {
cloneVNode,
Comment,
isSameVNodeType,
VNodeProps,
VNode,
mergeProps
} from '../vnode'
import { warn } from '../warning'
import { isKeepAlive } from './KeepAlive'
import { toRaw } from '@vue/reactivity'
import { onMounted } from '../apiLifecycle'
// Using camel case here makes it easier to use in render functions & JSX.
// In templates these will be written as @before-enter="xxx"
// The compiler has special handling to convert them into the proper cases.
export interface TransitionProps {
mode?: 'in-out' | 'out-in' | 'default'
appear?: boolean
// enter
onBeforeEnter?: (el: any) => void
onEnter?: (el: any, done: () => void) => void
onAfterEnter?: (el: any) => void
onEnterCancelled?: (el: any) => void
// leave
onBeforeLeave?: (el: any) => void
onLeave?: (el: any, done: () => void) => void
onAfterLeave?: (el: any) => void
onLeaveCancelled?: (el: any) => void
}
export const Transition = createComponent({
name: `Transition`,
setup(props: TransitionProps, { slots }) {
const instance = getCurrentInstance()!
let isLeaving = false
let isMounted = false
onMounted(() => {
isMounted = true
})
return () => {
const children = slots.default && slots.default()
if (!children || !children.length) {
return
}
// warn multiple elements
if (__DEV__ && children.length > 1) {
warn(
'<transition> can only be used on a single element. Use ' +
'<transition-group> for lists.'
)
}
// there's no need to track reactivity for these props so use the raw
// props for a bit better perf
const rawProps = toRaw(props)
const { mode } = rawProps
// check mode
if (__DEV__ && mode && !['in-out', 'out-in', 'default'].includes(mode)) {
warn(`invalid <transition> mode: ${mode}`)
}
// at this point children has a guaranteed length of 1.
const rawChild = children[0]
if (isLeaving) {
return placeholder(rawChild)
}
rawChild.transition = rawProps
// clone old subTree because we need to modify it
const oldChild = instance.subTree
? (instance.subTree = cloneVNode(instance.subTree))
: null
// handle mode
let performDelayedLeave: (() => void) | undefined
if (
oldChild &&
!isSameVNodeType(rawChild, oldChild) &&
oldChild.type !== Comment
) {
// update old tree's hooks in case of dynamic transition
oldChild.transition = rawProps
// switching between different views
if (mode === 'out-in') {
isLeaving = true
// return placeholder node and queue update when leave finishes
oldChild.props = mergeProps(oldChild.props!, {
onVnodeRemoved() {
isLeaving = false
instance.update()
}
})
return placeholder(rawChild)
} else if (mode === 'in-out') {
let delayedLeave: () => void
performDelayedLeave = () => delayedLeave()
oldChild.props = mergeProps(oldChild.props!, {
onVnodeDelayLeave(performLeave) {
delayedLeave = performLeave
}
})
}
}
return cloneVNode(
rawChild,
resolveTransitionInjections(rawProps, isMounted, performDelayedLeave)
)
}
}
})
if (__DEV__) {
;(Transition as any).props = {
mode: String,
appear: Boolean,
// enter
onBeforeEnter: Function,
onEnter: Function,
onAfterEnter: Function,
onEnterCancelled: Function,
// leave
onBeforeLeave: Function,
onLeave: Function,
onAfterLeave: Function,
onLeaveCancelled: Function
}
}
function resolveTransitionInjections(
{
appear,
onBeforeEnter,
onEnter,
onAfterEnter,
onEnterCancelled,
onBeforeLeave,
onLeave,
onAfterLeave,
onLeaveCancelled
}: TransitionProps,
isMounted: boolean,
performDelayedLeave?: () => void
): VNodeProps {
// TODO handle appear
// TODO handle cancel hooks
return {
onVnodeBeforeMount(vnode) {
if (!isMounted && !appear) {
return
}
onBeforeEnter && onBeforeEnter(vnode.el)
},
onVnodeMounted({ el }) {
if (!isMounted && !appear) {
return
}
const done = () => {
onAfterEnter && onAfterEnter(el)
performDelayedLeave && performDelayedLeave()
}
if (onEnter) {
onEnter(el, done)
} else {
done()
}
},
onVnodeBeforeRemove({ el }, remove) {
onBeforeLeave && onBeforeLeave(el)
if (onLeave) {
onLeave(el, () => {
remove()
onAfterLeave && onAfterLeave(el)
})
} else {
remove()
onAfterLeave && onAfterLeave(el)
}
}
}
}
// the placeholder really only handles one special case: KeepAlive
// in the case of a KeepAlive in a leave phase we need to return a KeepAlive
// placeholder with empty content to avoid the KeepAlive instance from being
// unmounted.
function placeholder(vnode: VNode): VNode | undefined {
if (isKeepAlive(vnode)) {
vnode = cloneVNode(vnode)
vnode.children = null
return vnode
}
}