2019-11-21 23:21:09 +08:00
|
|
|
import {
|
|
|
|
getCurrentInstance,
|
|
|
|
SetupContext,
|
|
|
|
ComponentOptions
|
|
|
|
} from '../component'
|
2019-11-21 10:56:17 +08:00
|
|
|
import { cloneVNode, Comment, isSameVNodeType, VNode } from '../vnode'
|
2019-11-21 07:04:44 +08:00
|
|
|
import { warn } from '../warning'
|
|
|
|
import { isKeepAlive } from './KeepAlive'
|
|
|
|
import { toRaw } from '@vue/reactivity'
|
2019-11-21 10:56:17 +08:00
|
|
|
import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
|
|
|
|
import { ShapeFlags } from '../shapeFlags'
|
2019-11-21 07:04:44 +08:00
|
|
|
|
|
|
|
export interface TransitionProps {
|
|
|
|
mode?: 'in-out' | 'out-in' | 'default'
|
|
|
|
appear?: boolean
|
2019-11-23 12:32:53 +08:00
|
|
|
|
2019-11-23 12:21:39 +08:00
|
|
|
// If true, indicates this is a transition that doesn't actually insert/remove
|
|
|
|
// the element, but toggles the show / hidden status instead.
|
|
|
|
// The transition hooks are injected, but will be skipped by the renderer.
|
|
|
|
// Instead, a custom directive can control the transition by calling the
|
|
|
|
// injected hooks (e.g. v-show).
|
|
|
|
persisted?: boolean
|
2019-11-23 12:32:53 +08:00
|
|
|
|
|
|
|
// Hooks. Using camel casef for easier usage 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.
|
2019-11-21 07:04:44 +08:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2019-11-23 06:10:17 +08:00
|
|
|
type TransitionHookCaller = (
|
|
|
|
hook: ((el: any) => void) | undefined,
|
|
|
|
args?: any[]
|
|
|
|
) => void
|
|
|
|
|
|
|
|
interface PendingCallbacks {
|
|
|
|
enter?: (cancelled?: boolean) => void
|
|
|
|
leave?: (cancelled?: boolean) => void
|
|
|
|
}
|
|
|
|
|
2019-11-21 23:21:09 +08:00
|
|
|
const TransitionImpl = {
|
2019-11-23 02:15:59 +08:00
|
|
|
name: `BaseTransition`,
|
2019-11-21 23:21:09 +08:00
|
|
|
setup(props: TransitionProps, { slots }: SetupContext) {
|
2019-11-21 07:04:44 +08:00
|
|
|
const instance = getCurrentInstance()!
|
2019-11-23 06:10:17 +08:00
|
|
|
const pendingCallbacks: PendingCallbacks = {}
|
2019-11-23 12:32:53 +08:00
|
|
|
let isLeaving = false
|
2019-11-21 07:04:44 +08:00
|
|
|
|
2019-11-23 06:10:17 +08:00
|
|
|
const callTransitionHook: TransitionHookCaller = (hook, args) => {
|
|
|
|
hook &&
|
|
|
|
callWithAsyncErrorHandling(
|
|
|
|
hook,
|
|
|
|
instance,
|
|
|
|
ErrorCodes.TRANSITION_HOOK,
|
|
|
|
args
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-11-21 07:04:44 +08:00
|
|
|
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.
|
2019-11-21 10:56:17 +08:00
|
|
|
const child = children[0]
|
2019-11-21 07:04:44 +08:00
|
|
|
if (isLeaving) {
|
2019-11-21 10:56:17 +08:00
|
|
|
return placeholder(child)
|
2019-11-21 07:04:44 +08:00
|
|
|
}
|
|
|
|
|
2019-11-21 10:56:17 +08:00
|
|
|
let delayedLeave: (() => void) | undefined
|
|
|
|
const performDelayedLeave = () => delayedLeave && delayedLeave()
|
2019-11-23 06:10:17 +08:00
|
|
|
const transitionHooks = (child.transition = resolveTransitionHooks(
|
2019-11-21 10:56:17 +08:00
|
|
|
rawProps,
|
2019-11-23 06:10:17 +08:00
|
|
|
callTransitionHook,
|
2019-11-23 12:32:53 +08:00
|
|
|
instance.isMounted,
|
2019-11-23 06:10:17 +08:00
|
|
|
pendingCallbacks,
|
2019-11-21 10:56:17 +08:00
|
|
|
performDelayedLeave
|
|
|
|
))
|
|
|
|
|
2019-11-21 07:04:44 +08:00
|
|
|
// clone old subTree because we need to modify it
|
|
|
|
const oldChild = instance.subTree
|
|
|
|
? (instance.subTree = cloneVNode(instance.subTree))
|
|
|
|
: null
|
|
|
|
|
|
|
|
// handle mode
|
|
|
|
if (
|
|
|
|
oldChild &&
|
2019-11-21 10:56:17 +08:00
|
|
|
!isSameVNodeType(child, oldChild) &&
|
2019-11-21 07:04:44 +08:00
|
|
|
oldChild.type !== Comment
|
|
|
|
) {
|
|
|
|
// update old tree's hooks in case of dynamic transition
|
2019-11-21 10:56:17 +08:00
|
|
|
// need to do this recursively in case of HOCs
|
2019-11-23 06:10:17 +08:00
|
|
|
updateHOCTransitionData(oldChild, transitionHooks)
|
2019-11-21 07:04:44 +08:00
|
|
|
// switching between different views
|
|
|
|
if (mode === 'out-in') {
|
|
|
|
isLeaving = true
|
|
|
|
// return placeholder node and queue update when leave finishes
|
2019-11-23 06:10:17 +08:00
|
|
|
transitionHooks.afterLeave = () => {
|
2019-11-21 10:56:17 +08:00
|
|
|
isLeaving = false
|
|
|
|
instance.update()
|
|
|
|
}
|
|
|
|
return placeholder(child)
|
2019-11-21 07:04:44 +08:00
|
|
|
} else if (mode === 'in-out') {
|
2019-11-23 06:10:17 +08:00
|
|
|
transitionHooks.delayLeave = performLeave => {
|
2019-11-21 10:56:17 +08:00
|
|
|
delayedLeave = performLeave
|
|
|
|
}
|
2019-11-21 07:04:44 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-21 10:56:17 +08:00
|
|
|
return child
|
2019-11-21 07:04:44 +08:00
|
|
|
}
|
|
|
|
}
|
2019-11-21 23:21:09 +08:00
|
|
|
}
|
2019-11-21 07:04:44 +08:00
|
|
|
|
|
|
|
if (__DEV__) {
|
2019-11-21 23:21:09 +08:00
|
|
|
;(TransitionImpl as ComponentOptions).props = {
|
2019-11-21 07:04:44 +08:00
|
|
|
mode: String,
|
|
|
|
appear: Boolean,
|
2019-11-23 12:21:39 +08:00
|
|
|
persisted: Boolean,
|
2019-11-21 07:04:44 +08:00
|
|
|
// enter
|
|
|
|
onBeforeEnter: Function,
|
|
|
|
onEnter: Function,
|
|
|
|
onAfterEnter: Function,
|
|
|
|
onEnterCancelled: Function,
|
|
|
|
// leave
|
|
|
|
onBeforeLeave: Function,
|
|
|
|
onLeave: Function,
|
|
|
|
onAfterLeave: Function,
|
|
|
|
onLeaveCancelled: Function
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-21 23:21:09 +08:00
|
|
|
// export the public type for h/tsx inference
|
|
|
|
// also to avoid inline import() in generated d.ts files
|
|
|
|
export const Transition = (TransitionImpl as any) as {
|
|
|
|
new (): {
|
|
|
|
$props: TransitionProps
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-23 06:10:17 +08:00
|
|
|
export interface TransitionHooks {
|
2019-11-23 12:21:39 +08:00
|
|
|
persisted: boolean
|
2019-11-21 10:56:17 +08:00
|
|
|
beforeEnter(el: object): void
|
|
|
|
enter(el: object): void
|
|
|
|
leave(el: object, remove: () => void): void
|
|
|
|
afterLeave?(): void
|
|
|
|
delayLeave?(performLeave: () => void): void
|
|
|
|
}
|
|
|
|
|
2019-11-23 07:09:26 +08:00
|
|
|
// The transition hooks are attached to the vnode as vnode.transition
|
|
|
|
// and will be called at appropriate timing in the renderer.
|
2019-11-23 06:10:17 +08:00
|
|
|
function resolveTransitionHooks(
|
2019-11-21 07:04:44 +08:00
|
|
|
{
|
|
|
|
appear,
|
2019-11-23 12:21:39 +08:00
|
|
|
persisted = false,
|
2019-11-21 07:04:44 +08:00
|
|
|
onBeforeEnter,
|
|
|
|
onEnter,
|
|
|
|
onAfterEnter,
|
|
|
|
onEnterCancelled,
|
|
|
|
onBeforeLeave,
|
|
|
|
onLeave,
|
|
|
|
onAfterLeave,
|
|
|
|
onLeaveCancelled
|
|
|
|
}: TransitionProps,
|
2019-11-23 06:10:17 +08:00
|
|
|
callHook: TransitionHookCaller,
|
2019-11-21 07:04:44 +08:00
|
|
|
isMounted: boolean,
|
2019-11-23 06:10:17 +08:00
|
|
|
pendingCallbacks: PendingCallbacks,
|
2019-11-21 10:56:17 +08:00
|
|
|
performDelayedLeave: () => void
|
2019-11-23 06:10:17 +08:00
|
|
|
): TransitionHooks {
|
2019-11-21 07:04:44 +08:00
|
|
|
return {
|
2019-11-23 12:21:39 +08:00
|
|
|
persisted,
|
2019-11-21 10:56:17 +08:00
|
|
|
beforeEnter(el) {
|
2019-11-21 07:04:44 +08:00
|
|
|
if (!isMounted && !appear) {
|
|
|
|
return
|
|
|
|
}
|
2019-11-23 06:10:17 +08:00
|
|
|
if (pendingCallbacks.leave) {
|
|
|
|
pendingCallbacks.leave(true /* cancelled */)
|
|
|
|
}
|
|
|
|
callHook(onBeforeEnter, [el])
|
2019-11-21 07:04:44 +08:00
|
|
|
},
|
2019-11-23 06:10:17 +08:00
|
|
|
|
2019-11-21 10:56:17 +08:00
|
|
|
enter(el) {
|
2019-11-21 07:04:44 +08:00
|
|
|
if (!isMounted && !appear) {
|
|
|
|
return
|
|
|
|
}
|
2019-11-23 12:21:39 +08:00
|
|
|
let called = false
|
2019-11-23 06:10:17 +08:00
|
|
|
const afterEnter = (pendingCallbacks.enter = (cancelled?) => {
|
2019-11-23 12:21:39 +08:00
|
|
|
if (called) return
|
|
|
|
called = true
|
2019-11-23 06:10:17 +08:00
|
|
|
if (cancelled) {
|
|
|
|
callHook(onEnterCancelled, [el])
|
|
|
|
} else {
|
|
|
|
callHook(onAfterEnter, [el])
|
|
|
|
performDelayedLeave()
|
|
|
|
}
|
|
|
|
pendingCallbacks.enter = undefined
|
|
|
|
})
|
2019-11-21 07:04:44 +08:00
|
|
|
if (onEnter) {
|
2019-11-23 06:10:17 +08:00
|
|
|
onEnter(el, afterEnter)
|
2019-11-21 07:04:44 +08:00
|
|
|
} else {
|
2019-11-23 06:10:17 +08:00
|
|
|
afterEnter()
|
2019-11-21 07:04:44 +08:00
|
|
|
}
|
|
|
|
},
|
2019-11-23 06:10:17 +08:00
|
|
|
|
2019-11-21 10:56:17 +08:00
|
|
|
leave(el, remove) {
|
2019-11-23 06:10:17 +08:00
|
|
|
if (pendingCallbacks.enter) {
|
|
|
|
pendingCallbacks.enter(true /* cancelled */)
|
|
|
|
}
|
|
|
|
callHook(onBeforeLeave, [el])
|
2019-11-23 12:21:39 +08:00
|
|
|
let called = false
|
2019-11-23 06:10:17 +08:00
|
|
|
const afterLeave = (pendingCallbacks.leave = (cancelled?) => {
|
2019-11-23 12:21:39 +08:00
|
|
|
if (called) return
|
|
|
|
called = true
|
2019-11-23 06:10:17 +08:00
|
|
|
remove()
|
|
|
|
if (cancelled) {
|
|
|
|
callHook(onLeaveCancelled, [el])
|
|
|
|
} else {
|
|
|
|
callHook(onAfterLeave, [el])
|
|
|
|
}
|
|
|
|
pendingCallbacks.leave = undefined
|
|
|
|
})
|
2019-11-21 07:04:44 +08:00
|
|
|
if (onLeave) {
|
2019-11-23 06:10:17 +08:00
|
|
|
onLeave(el, afterLeave)
|
2019-11-21 07:04:44 +08:00
|
|
|
} else {
|
2019-11-21 10:56:17 +08:00
|
|
|
afterLeave()
|
2019-11-21 07:04:44 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
|
|
|
}
|
|
|
|
}
|
2019-11-21 10:56:17 +08:00
|
|
|
|
2019-11-23 06:10:17 +08:00
|
|
|
function updateHOCTransitionData(vnode: VNode, data: TransitionHooks) {
|
2019-11-21 10:56:17 +08:00
|
|
|
if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
|
|
|
|
updateHOCTransitionData(vnode.component!.subTree, data)
|
|
|
|
} else {
|
|
|
|
vnode.transition = data
|
|
|
|
}
|
|
|
|
}
|