2019-11-21 23:21:09 +08:00
|
|
|
import {
|
|
|
|
getCurrentInstance,
|
|
|
|
SetupContext,
|
2019-11-29 07:41:01 +08:00
|
|
|
ComponentOptions,
|
|
|
|
ComponentInternalInstance
|
2019-11-21 23:21:09 +08:00
|
|
|
} from '../component'
|
2019-11-26 06:34:28 +08:00
|
|
|
import {
|
|
|
|
cloneVNode,
|
|
|
|
Comment,
|
|
|
|
isSameVNodeType,
|
|
|
|
VNode,
|
2020-01-29 11:58:02 +08:00
|
|
|
VNodeArrayChildren
|
2019-11-26 06:34:28 +08:00
|
|
|
} 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-25 07:37:59 +08:00
|
|
|
import { onBeforeUnmount, onMounted } from '../apiLifecycle'
|
2019-11-21 07:04:44 +08:00
|
|
|
|
2019-11-25 05:00:46 +08:00
|
|
|
export interface BaseTransitionProps {
|
2019-11-21 07:04:44 +08:00
|
|
|
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
|
|
|
|
2019-11-26 16:26:03 +08:00
|
|
|
// Hooks. Using camel case for easier usage in render functions & JSX.
|
2019-11-25 07:37:59 +08:00
|
|
|
// In templates these can be written as @before-enter="xxx" as prop names
|
2019-11-26 16:26:03 +08:00
|
|
|
// are camelized.
|
2019-11-21 07:04:44 +08:00
|
|
|
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
|
2019-11-27 07:07:05 +08:00
|
|
|
onLeaveCancelled?: (el: any) => void // only fired in persisted mode
|
2019-11-21 07:04:44 +08:00
|
|
|
}
|
|
|
|
|
2019-11-26 06:34:28 +08:00
|
|
|
export interface TransitionHooks {
|
|
|
|
persisted: boolean
|
|
|
|
beforeEnter(el: object): void
|
|
|
|
enter(el: object): void
|
|
|
|
leave(el: object, remove: () => void): void
|
|
|
|
afterLeave?(): void
|
2019-11-28 06:54:41 +08:00
|
|
|
delayLeave?(
|
|
|
|
el: object,
|
|
|
|
earlyRemove: () => void,
|
|
|
|
delayedLeave: () => void
|
|
|
|
): void
|
2019-11-26 06:34:28 +08:00
|
|
|
delayedLeave?(): void
|
|
|
|
}
|
|
|
|
|
2019-11-23 06:10:17 +08:00
|
|
|
type TransitionHookCaller = (
|
|
|
|
hook: ((el: any) => void) | undefined,
|
|
|
|
args?: any[]
|
|
|
|
) => void
|
|
|
|
|
2019-11-29 07:41:01 +08:00
|
|
|
export type PendingCallback = (cancelled?: boolean) => void
|
2019-11-26 06:34:28 +08:00
|
|
|
|
2019-11-29 07:41:01 +08:00
|
|
|
export interface TransitionState {
|
2019-11-25 07:37:59 +08:00
|
|
|
isMounted: boolean
|
|
|
|
isLeaving: boolean
|
|
|
|
isUnmounting: boolean
|
2019-11-26 06:34:28 +08:00
|
|
|
// Track pending leave callbacks for children of the same key.
|
|
|
|
// This is used to force remove leaving a child when a new copy is entering.
|
2019-11-28 01:17:16 +08:00
|
|
|
leavingVNodes: Map<any, Record<string, VNode>>
|
2019-11-26 06:34:28 +08:00
|
|
|
}
|
|
|
|
|
2019-11-29 07:41:01 +08:00
|
|
|
export interface TransitionElement {
|
2019-11-26 06:34:28 +08:00
|
|
|
// in persisted mode (e.g. v-show), the same element is toggled, so the
|
|
|
|
// pending enter/leave callbacks may need to cancalled if the state is toggled
|
|
|
|
// before it finishes.
|
|
|
|
_enterCb?: PendingCallback
|
|
|
|
_leaveCb?: PendingCallback
|
2019-11-23 06:10:17 +08:00
|
|
|
}
|
|
|
|
|
2019-11-29 07:41:01 +08:00
|
|
|
export function useTransitionState(): TransitionState {
|
|
|
|
const state: TransitionState = {
|
|
|
|
isMounted: false,
|
|
|
|
isLeaving: false,
|
|
|
|
isUnmounting: false,
|
|
|
|
leavingVNodes: new Map()
|
|
|
|
}
|
|
|
|
onMounted(() => {
|
|
|
|
state.isMounted = true
|
|
|
|
})
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
state.isUnmounting = true
|
|
|
|
})
|
|
|
|
return state
|
|
|
|
}
|
|
|
|
|
2019-11-25 05:00:46 +08:00
|
|
|
const BaseTransitionImpl = {
|
2019-11-23 02:15:59 +08:00
|
|
|
name: `BaseTransition`,
|
2019-11-25 05:00:46 +08:00
|
|
|
setup(props: BaseTransitionProps, { slots }: SetupContext) {
|
2019-11-21 07:04:44 +08:00
|
|
|
const instance = getCurrentInstance()!
|
2019-11-29 07:41:01 +08:00
|
|
|
const state = useTransitionState()
|
2019-11-23 06:10:17 +08:00
|
|
|
|
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(
|
2019-11-26 06:34:28 +08:00
|
|
|
'<transition> can only be used on a single element or component. Use ' +
|
2019-11-21 07:04:44 +08:00
|
|
|
'<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-25 07:37:59 +08:00
|
|
|
if (state.isLeaving) {
|
2019-11-26 06:34:28 +08:00
|
|
|
return emptyPlaceholder(child)
|
2019-11-21 07:04:44 +08:00
|
|
|
}
|
|
|
|
|
2019-11-26 06:34:28 +08:00
|
|
|
// in the case of <transition><keep-alive/></transition>, we need to
|
|
|
|
// compare the type of the kept-alive children.
|
|
|
|
const innerChild = getKeepAliveChild(child)
|
|
|
|
if (!innerChild) {
|
|
|
|
return emptyPlaceholder(child)
|
|
|
|
}
|
2019-11-25 07:37:59 +08:00
|
|
|
|
2019-11-26 06:34:28 +08:00
|
|
|
const enterHooks = (innerChild.transition = resolveTransitionHooks(
|
|
|
|
innerChild,
|
2019-11-21 10:56:17 +08:00
|
|
|
rawProps,
|
2019-11-25 07:37:59 +08:00
|
|
|
state,
|
2019-11-29 07:41:01 +08:00
|
|
|
instance
|
2019-11-21 10:56:17 +08:00
|
|
|
))
|
|
|
|
|
2019-11-21 07:04:44 +08:00
|
|
|
const oldChild = instance.subTree
|
2019-11-26 06:34:28 +08:00
|
|
|
const oldInnerChild = oldChild && getKeepAliveChild(oldChild)
|
2019-11-21 07:04:44 +08:00
|
|
|
// handle mode
|
|
|
|
if (
|
2019-11-26 06:34:28 +08:00
|
|
|
oldInnerChild &&
|
|
|
|
oldInnerChild.type !== Comment &&
|
|
|
|
!isSameVNodeType(innerChild, oldInnerChild)
|
2019-11-21 07:04:44 +08:00
|
|
|
) {
|
2019-11-26 06:34:28 +08:00
|
|
|
const prevHooks = oldInnerChild.transition!
|
|
|
|
const leavingHooks = resolveTransitionHooks(
|
|
|
|
oldInnerChild,
|
|
|
|
rawProps,
|
|
|
|
state,
|
2019-11-29 07:41:01 +08:00
|
|
|
instance
|
2019-11-26 06:34:28 +08:00
|
|
|
)
|
2019-11-21 07:04:44 +08:00
|
|
|
// update old tree's hooks in case of dynamic transition
|
2019-11-26 06:34:28 +08:00
|
|
|
setTransitionHooks(oldInnerChild, leavingHooks)
|
2019-11-21 07:04:44 +08:00
|
|
|
// switching between different views
|
|
|
|
if (mode === 'out-in') {
|
2019-11-25 07:37:59 +08:00
|
|
|
state.isLeaving = true
|
2019-11-21 07:04:44 +08:00
|
|
|
// return placeholder node and queue update when leave finishes
|
2019-11-26 06:34:28 +08:00
|
|
|
leavingHooks.afterLeave = () => {
|
2019-11-25 07:37:59 +08:00
|
|
|
state.isLeaving = false
|
2019-11-21 10:56:17 +08:00
|
|
|
instance.update()
|
|
|
|
}
|
2019-11-26 06:34:28 +08:00
|
|
|
return emptyPlaceholder(child)
|
2019-11-21 07:04:44 +08:00
|
|
|
} else if (mode === 'in-out') {
|
2019-11-26 06:34:28 +08:00
|
|
|
delete prevHooks.delayedLeave
|
2019-11-28 06:54:41 +08:00
|
|
|
leavingHooks.delayLeave = (
|
|
|
|
el: TransitionElement,
|
|
|
|
earlyRemove,
|
|
|
|
delayedLeave
|
|
|
|
) => {
|
|
|
|
const leavingVNodesCache = getLeavingNodesForType(
|
|
|
|
state,
|
|
|
|
oldInnerChild
|
|
|
|
)
|
|
|
|
leavingVNodesCache[String(oldInnerChild.key)] = oldInnerChild
|
|
|
|
// early removal callback
|
|
|
|
el._leaveCb = () => {
|
|
|
|
earlyRemove()
|
|
|
|
el._leaveCb = undefined
|
|
|
|
delete enterHooks.delayedLeave
|
|
|
|
}
|
2019-11-26 06:34:28 +08:00
|
|
|
enterHooks.delayedLeave = delayedLeave
|
2019-11-21 10:56:17 +08:00
|
|
|
}
|
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-25 05:00:46 +08:00
|
|
|
;(BaseTransitionImpl 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
|
2019-11-25 05:00:46 +08:00
|
|
|
export const BaseTransition = (BaseTransitionImpl as any) as {
|
2019-11-21 23:21:09 +08:00
|
|
|
new (): {
|
2019-11-25 05:00:46 +08:00
|
|
|
$props: BaseTransitionProps
|
2019-11-21 23:21:09 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-11-28 06:54:41 +08:00
|
|
|
function getLeavingNodesForType(
|
|
|
|
state: TransitionState,
|
|
|
|
vnode: VNode
|
|
|
|
): Record<string, VNode> {
|
|
|
|
const { leavingVNodes } = state
|
|
|
|
let leavingVNodesCache = leavingVNodes.get(vnode.type)!
|
|
|
|
if (!leavingVNodesCache) {
|
|
|
|
leavingVNodesCache = Object.create(null)
|
|
|
|
leavingVNodes.set(vnode.type, leavingVNodesCache)
|
|
|
|
}
|
|
|
|
return leavingVNodesCache
|
|
|
|
}
|
|
|
|
|
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-29 07:41:01 +08:00
|
|
|
export function resolveTransitionHooks(
|
2019-11-26 06:34:28 +08:00
|
|
|
vnode: VNode,
|
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
|
2019-11-25 05:00:46 +08:00
|
|
|
}: BaseTransitionProps,
|
2019-11-25 07:37:59 +08:00
|
|
|
state: TransitionState,
|
2019-11-29 07:41:01 +08:00
|
|
|
instance: ComponentInternalInstance
|
2019-11-23 06:10:17 +08:00
|
|
|
): TransitionHooks {
|
2019-11-26 06:34:28 +08:00
|
|
|
const key = String(vnode.key)
|
2019-11-28 06:54:41 +08:00
|
|
|
const leavingVNodesCache = getLeavingNodesForType(state, vnode)
|
2019-11-26 06:34:28 +08:00
|
|
|
|
2019-11-29 07:41:01 +08:00
|
|
|
const callHook: TransitionHookCaller = (hook, args) => {
|
|
|
|
hook &&
|
|
|
|
callWithAsyncErrorHandling(
|
|
|
|
hook,
|
|
|
|
instance,
|
|
|
|
ErrorCodes.TRANSITION_HOOK,
|
|
|
|
args
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2019-11-26 06:34:28 +08:00
|
|
|
const hooks: TransitionHooks = {
|
2019-11-23 12:21:39 +08:00
|
|
|
persisted,
|
2019-11-26 06:34:28 +08:00
|
|
|
beforeEnter(el: TransitionElement) {
|
2019-11-25 07:37:59 +08:00
|
|
|
if (!appear && !state.isMounted) {
|
|
|
|
return
|
2019-11-23 06:10:17 +08:00
|
|
|
}
|
2019-11-26 06:34:28 +08:00
|
|
|
// for same element (v-show)
|
|
|
|
if (el._leaveCb) {
|
|
|
|
el._leaveCb(true /* cancelled */)
|
|
|
|
}
|
|
|
|
// for toggled element with same key (v-if)
|
2019-11-28 01:17:16 +08:00
|
|
|
const leavingVNode = leavingVNodesCache[key]
|
2019-11-26 06:34:28 +08:00
|
|
|
if (
|
|
|
|
leavingVNode &&
|
|
|
|
isSameVNodeType(vnode, leavingVNode) &&
|
|
|
|
leavingVNode.el._leaveCb
|
|
|
|
) {
|
|
|
|
// force early removal (not cancelled)
|
|
|
|
leavingVNode.el._leaveCb()
|
|
|
|
}
|
2019-11-23 06:10:17 +08:00
|
|
|
callHook(onBeforeEnter, [el])
|
2019-11-21 07:04:44 +08:00
|
|
|
},
|
2019-11-23 06:10:17 +08:00
|
|
|
|
2019-11-26 06:34:28 +08:00
|
|
|
enter(el: TransitionElement) {
|
2019-11-25 07:37:59 +08:00
|
|
|
if (!appear && !state.isMounted) {
|
2019-11-21 07:04:44 +08:00
|
|
|
return
|
|
|
|
}
|
2019-11-23 12:21:39 +08:00
|
|
|
let called = false
|
2019-11-26 06:34:28 +08:00
|
|
|
const afterEnter = (el._enterCb = (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])
|
|
|
|
}
|
2019-11-26 06:34:28 +08:00
|
|
|
if (hooks.delayedLeave) {
|
|
|
|
hooks.delayedLeave()
|
|
|
|
}
|
|
|
|
el._enterCb = undefined
|
2019-11-23 06:10:17 +08:00
|
|
|
})
|
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-26 06:34:28 +08:00
|
|
|
leave(el: TransitionElement, remove) {
|
|
|
|
const key = String(vnode.key)
|
|
|
|
if (el._enterCb) {
|
|
|
|
el._enterCb(true /* cancelled */)
|
2019-11-25 07:37:59 +08:00
|
|
|
}
|
|
|
|
if (state.isUnmounting) {
|
|
|
|
return remove()
|
2019-11-23 06:10:17 +08:00
|
|
|
}
|
|
|
|
callHook(onBeforeLeave, [el])
|
2019-11-23 12:21:39 +08:00
|
|
|
let called = false
|
2019-11-26 06:34:28 +08:00
|
|
|
const afterLeave = (el._leaveCb = (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])
|
|
|
|
}
|
2019-11-26 06:34:28 +08:00
|
|
|
el._leaveCb = undefined
|
2019-11-28 01:17:16 +08:00
|
|
|
if (leavingVNodesCache[key] === vnode) {
|
|
|
|
delete leavingVNodesCache[key]
|
|
|
|
}
|
2019-11-23 06:10:17 +08:00
|
|
|
})
|
2019-11-28 01:17:16 +08:00
|
|
|
leavingVNodesCache[key] = vnode
|
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
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-11-26 06:34:28 +08:00
|
|
|
|
|
|
|
return hooks
|
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.
|
2019-11-26 06:34:28 +08:00
|
|
|
function emptyPlaceholder(vnode: VNode): VNode | undefined {
|
2019-11-21 07:04:44 +08:00
|
|
|
if (isKeepAlive(vnode)) {
|
|
|
|
vnode = cloneVNode(vnode)
|
|
|
|
vnode.children = null
|
|
|
|
return vnode
|
|
|
|
}
|
|
|
|
}
|
2019-11-21 10:56:17 +08:00
|
|
|
|
2019-11-26 06:34:28 +08:00
|
|
|
function getKeepAliveChild(vnode: VNode): VNode | undefined {
|
|
|
|
return isKeepAlive(vnode)
|
|
|
|
? vnode.children
|
2020-01-29 11:58:02 +08:00
|
|
|
? ((vnode.children as VNodeArrayChildren)[0] as VNode)
|
2019-11-26 06:34:28 +08:00
|
|
|
: undefined
|
|
|
|
: vnode
|
|
|
|
}
|
|
|
|
|
|
|
|
export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks) {
|
|
|
|
if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) {
|
|
|
|
setTransitionHooks(vnode.component.subTree, hooks)
|
2019-11-21 10:56:17 +08:00
|
|
|
} else {
|
2019-11-26 06:34:28 +08:00
|
|
|
vnode.transition = hooks
|
2019-11-21 10:56:17 +08:00
|
|
|
}
|
|
|
|
}
|