| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468 |
- import {
- getCurrentInstance,
- SetupContext,
- ComponentInternalInstance
- } from '../component'
- import {
- cloneVNode,
- Comment,
- isSameVNodeType,
- VNode,
- VNodeArrayChildren,
- Fragment
- } from '../vnode'
- import { warn } from '../warning'
- import { isKeepAlive } from './KeepAlive'
- import { toRaw } from '@vue/reactivity'
- import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
- import { ShapeFlags, PatchFlags } from '@vue/shared'
- import { onBeforeUnmount, onMounted } from '../apiLifecycle'
- import { RendererElement } from '../renderer'
- export interface BaseTransitionProps<HostElement = RendererElement> {
- 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: HostElement) => void
- onEnter?: (el: HostElement, done: () => void) => void
- onAfterEnter?: (el: HostElement) => void
- onEnterCancelled?: (el: HostElement) => void
- // leave
- onBeforeLeave?: (el: HostElement) => void
- onLeave?: (el: HostElement, done: () => void) => void
- onAfterLeave?: (el: HostElement) => void
- onLeaveCancelled?: (el: HostElement) => void // only fired in persisted mode
- // appear
- onBeforeAppear?: (el: HostElement) => void
- onAppear?: (el: HostElement, done: () => void) => void
- onAfterAppear?: (el: HostElement) => void
- onAppearCancelled?: (el: HostElement) => void
- }
- export interface TransitionHooks<
- HostElement extends RendererElement = RendererElement
- > {
- persisted: boolean
- beforeEnter(el: HostElement): void
- enter(el: HostElement): void
- leave(el: HostElement, remove: () => void): void
- afterLeave?(): void
- delayLeave?(
- el: HostElement,
- 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 be cancelled 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 TransitionHookValidator = [Function, Array]
- const BaseTransitionImpl = {
- name: `BaseTransition`,
- props: {
- mode: String,
- appear: Boolean,
- persisted: Boolean,
- // enter
- onBeforeEnter: TransitionHookValidator,
- onEnter: TransitionHookValidator,
- onAfterEnter: TransitionHookValidator,
- onEnterCancelled: TransitionHookValidator,
- // leave
- onBeforeLeave: TransitionHookValidator,
- onLeave: TransitionHookValidator,
- onAfterLeave: TransitionHookValidator,
- onLeaveCancelled: TransitionHookValidator,
- // appear
- onBeforeAppear: TransitionHookValidator,
- onAppear: TransitionHookValidator,
- onAfterAppear: TransitionHookValidator,
- onAppearCancelled: TransitionHookValidator
- },
- setup(props: BaseTransitionProps, { slots }: SetupContext) {
- const instance = getCurrentInstance()!
- const state = useTransitionState()
- let prevTransitionKey: any
- return () => {
- const children =
- slots.default && getTransitionRawChildren(slots.default(), true)
- 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)
- let transitionKeyChanged = false
- const { getTransitionKey } = innerChild.type as any
- if (getTransitionKey) {
- const key = getTransitionKey()
- if (prevTransitionKey === undefined) {
- prevTransitionKey = key
- } else if (key !== prevTransitionKey) {
- prevTransitionKey = key
- transitionKeyChanged = true
- }
- }
- // handle mode
- if (
- oldInnerChild &&
- oldInnerChild.type !== Comment &&
- (!isSameVNodeType(innerChild, oldInnerChild) || transitionKeyChanged)
- ) {
- 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') {
- 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
- }
- }
- }
- // 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<any>
- }
- }
- 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,
- onBeforeAppear,
- onAppear,
- onAfterAppear,
- onAppearCancelled
- }: BaseTransitionProps<any>,
- 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<TransitionElement> = {
- persisted,
- beforeEnter(el) {
- let hook = onBeforeEnter
- if (!state.isMounted) {
- if (appear) {
- hook = onBeforeAppear || onBeforeEnter
- } else {
- 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(hook, [el])
- },
- enter(el) {
- let hook = onEnter
- let afterHook = onAfterEnter
- let cancelHook = onEnterCancelled
- if (!state.isMounted) {
- if (appear) {
- hook = onAppear || onEnter
- afterHook = onAfterAppear || onAfterEnter
- cancelHook = onAppearCancelled || onEnterCancelled
- } else {
- return
- }
- }
- let called = false
- const done = (el._enterCb = (cancelled?) => {
- if (called) return
- called = true
- if (cancelled) {
- callHook(cancelHook, [el])
- } else {
- callHook(afterHook, [el])
- }
- if (hooks.delayedLeave) {
- hooks.delayedLeave()
- }
- el._enterCb = undefined
- })
- if (hook) {
- hook(el, done)
- if (hook.length <= 1) {
- done()
- }
- } else {
- done()
- }
- },
- leave(el, 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 done = (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, done)
- if (onLeave.length <= 1) {
- done()
- }
- } else {
- done()
- }
- }
- }
- 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
- }
- }
- export function getTransitionRawChildren(
- children: VNode[],
- keepComment: boolean = false
- ): VNode[] {
- let ret: VNode[] = []
- let keyedFragmentCount = 0
- for (let i = 0; i < children.length; i++) {
- const child = children[i]
- // handle fragment children case, e.g. v-for
- if (child.type === Fragment) {
- if (child.patchFlag & PatchFlags.KEYED_FRAGMENT) keyedFragmentCount++
- ret = ret.concat(
- getTransitionRawChildren(child.children as VNode[], keepComment)
- )
- }
- // comment placeholders should be skipped, e.g. v-if
- else if (keepComment || child.type !== Comment) {
- ret.push(child)
- }
- }
- // #1126 if a transition children list contains multiple sub fragments, these
- // fragments will be merged into a flat children array. Since each v-for
- // fragment may contain different static bindings inside, we need to de-top
- // these children to force full diffs to ensure correct behavior.
- if (keyedFragmentCount > 1) {
- for (let i = 0; i < ret.length; i++) {
- ret[i].patchFlag = PatchFlags.BAIL
- }
- }
- return ret
- }
|