import { getCurrentInstance, SetupContext, ComponentOptions, ComponentInternalInstance } from '../component' import { cloneVNode, Comment, isSameVNodeType, VNode, VNodeArrayChildren } from '../vnode' import { warn } from '../warning' import { isKeepAlive } from './KeepAlive' import { toRaw } from '@vue/reactivity' import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling' import { ShapeFlags } from '@vue/shared' import { onBeforeUnmount, onMounted } from '../apiLifecycle' export interface BaseTransitionProps { mode?: 'in-out' | 'out-in' | 'default' appear?: boolean // 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 // Hooks. Using camel case for easier usage in render functions & JSX. // In templates these can be written as @before-enter="xxx" as prop names // are camelized. 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 // only fired in persisted mode } export interface TransitionHooks { persisted: boolean beforeEnter(el: object): void enter(el: object): void leave(el: object, remove: () => void): void afterLeave?(): void delayLeave?( el: object, earlyRemove: () => void, delayedLeave: () => void ): void delayedLeave?(): void } type TransitionHookCaller = ( hook: ((el: any) => void) | undefined, args?: any[] ) => void export type PendingCallback = (cancelled?: boolean) => void export interface TransitionState { isMounted: boolean isLeaving: boolean isUnmounting: boolean // 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. leavingVNodes: Map> } export interface TransitionElement { // 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 } 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 } const BaseTransitionImpl = { name: `BaseTransition`, setup(props: BaseTransitionProps, { slots }: SetupContext) { const instance = getCurrentInstance()! const state = useTransitionState() return () => { const children = slots.default && slots.default() if (!children || !children.length) { return } // warn multiple elements if (__DEV__ && children.length > 1) { warn( ' can only be used on a single element or component. Use ' + ' 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 mode: ${mode}`) } // at this point children has a guaranteed length of 1. const child = children[0] if (state.isLeaving) { return emptyPlaceholder(child) } // in the case of , we need to // compare the type of the kept-alive children. const innerChild = getKeepAliveChild(child) if (!innerChild) { return emptyPlaceholder(child) } const enterHooks = (innerChild.transition = resolveTransitionHooks( innerChild, rawProps, state, instance )) const oldChild = instance.subTree const oldInnerChild = oldChild && getKeepAliveChild(oldChild) // handle mode if ( oldInnerChild && oldInnerChild.type !== Comment && !isSameVNodeType(innerChild, oldInnerChild) ) { const prevHooks = oldInnerChild.transition! const leavingHooks = resolveTransitionHooks( oldInnerChild, rawProps, state, instance ) // update old tree's hooks in case of dynamic transition setTransitionHooks(oldInnerChild, leavingHooks) // switching between different views if (mode === 'out-in') { state.isLeaving = true // return placeholder node and queue update when leave finishes leavingHooks.afterLeave = () => { state.isLeaving = false instance.update() } return emptyPlaceholder(child) } else if (mode === 'in-out') { delete prevHooks.delayedLeave 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 } enterHooks.delayedLeave = delayedLeave } } } return child } } } if (__DEV__) { ;(BaseTransitionImpl as ComponentOptions).props = { mode: String, appear: Boolean, persisted: Boolean, // enter onBeforeEnter: Function, onEnter: Function, onAfterEnter: Function, onEnterCancelled: Function, // leave onBeforeLeave: Function, onLeave: Function, onAfterLeave: Function, onLeaveCancelled: Function } } // export the public type for h/tsx inference // also to avoid inline import() in generated d.ts files export const BaseTransition = (BaseTransitionImpl as any) as { new (): { $props: BaseTransitionProps } } function getLeavingNodesForType( state: TransitionState, vnode: VNode ): Record { const { leavingVNodes } = state let leavingVNodesCache = leavingVNodes.get(vnode.type)! if (!leavingVNodesCache) { leavingVNodesCache = Object.create(null) leavingVNodes.set(vnode.type, leavingVNodesCache) } return leavingVNodesCache } // The transition hooks are attached to the vnode as vnode.transition // and will be called at appropriate timing in the renderer. export function resolveTransitionHooks( vnode: VNode, { appear, persisted = false, onBeforeEnter, onEnter, onAfterEnter, onEnterCancelled, onBeforeLeave, onLeave, onAfterLeave, onLeaveCancelled }: BaseTransitionProps, state: TransitionState, instance: ComponentInternalInstance ): TransitionHooks { const key = String(vnode.key) const leavingVNodesCache = getLeavingNodesForType(state, vnode) const callHook: TransitionHookCaller = (hook, args) => { hook && callWithAsyncErrorHandling( hook, instance, ErrorCodes.TRANSITION_HOOK, args ) } const hooks: TransitionHooks = { persisted, beforeEnter(el: TransitionElement) { if (!appear && !state.isMounted) { return } // for same element (v-show) if (el._leaveCb) { el._leaveCb(true /* cancelled */) } // for toggled element with same key (v-if) const leavingVNode = leavingVNodesCache[key] if ( leavingVNode && isSameVNodeType(vnode, leavingVNode) && leavingVNode.el._leaveCb ) { // force early removal (not cancelled) leavingVNode.el._leaveCb() } callHook(onBeforeEnter, [el]) }, enter(el: TransitionElement) { if (!appear && !state.isMounted) { return } let called = false const afterEnter = (el._enterCb = (cancelled?) => { if (called) return called = true if (cancelled) { callHook(onEnterCancelled, [el]) } else { callHook(onAfterEnter, [el]) } if (hooks.delayedLeave) { hooks.delayedLeave() } el._enterCb = undefined }) if (onEnter) { onEnter(el, afterEnter) } else { afterEnter() } }, leave(el: TransitionElement, remove) { const key = String(vnode.key) if (el._enterCb) { el._enterCb(true /* cancelled */) } if (state.isUnmounting) { return remove() } callHook(onBeforeLeave, [el]) let called = false const afterLeave = (el._leaveCb = (cancelled?) => { if (called) return called = true remove() if (cancelled) { callHook(onLeaveCancelled, [el]) } else { callHook(onAfterLeave, [el]) } el._leaveCb = undefined if (leavingVNodesCache[key] === vnode) { delete leavingVNodesCache[key] } }) leavingVNodesCache[key] = vnode if (onLeave) { onLeave(el, afterLeave) } else { afterLeave() } } } return hooks } // 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 emptyPlaceholder(vnode: VNode): VNode | undefined { if (isKeepAlive(vnode)) { vnode = cloneVNode(vnode) vnode.children = null return vnode } } function getKeepAliveChild(vnode: VNode): VNode | undefined { return isKeepAlive(vnode) ? vnode.children ? ((vnode.children as VNodeArrayChildren)[0] as VNode) : undefined : vnode } export function setTransitionHooks(vnode: VNode, hooks: TransitionHooks) { if (vnode.shapeFlag & ShapeFlags.COMPONENT && vnode.component) { setTransitionHooks(vnode.component.subTree, hooks) } else { vnode.transition = hooks } }