| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384 |
- 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 '../shapeFlags'
- 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<any, Record<string, VNode>>
- }
- 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(
- '<transition> can only be used on a single element or component. 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 child = children[0]
- if (state.isLeaving) {
- return emptyPlaceholder(child)
- }
- // 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)
- }
- 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<string, VNode> {
- 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
- }
- }
|