vue3-yuanma/packages/runtime-core/src/components/Transition.ts

279 lines
7.6 KiB
TypeScript
Raw Normal View History

2019-11-21 23:21:09 +08:00
import {
getCurrentInstance,
SetupContext,
ComponentOptions
} from '../component'
import { cloneVNode, Comment, isSameVNodeType, VNode } from '../vnode'
import { warn } from '../warning'
import { isKeepAlive } from './KeepAlive'
import { toRaw } from '@vue/reactivity'
import { onMounted } from '../apiLifecycle'
import { callWithAsyncErrorHandling, ErrorCodes } from '../errorHandling'
import { ShapeFlags } from '../shapeFlags'
// Using camel case here makes it easier to use 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.
export interface TransitionProps {
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
// 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 = {
name: `BaseTransition`,
2019-11-21 23:21:09 +08:00
setup(props: TransitionProps, { slots }: SetupContext) {
const instance = getCurrentInstance()!
let isLeaving = false
let isMounted = false
2019-11-23 06:10:17 +08:00
const pendingCallbacks: PendingCallbacks = {}
onMounted(() => {
isMounted = true
})
2019-11-23 06:10:17 +08:00
const callTransitionHook: TransitionHookCaller = (hook, args) => {
hook &&
callWithAsyncErrorHandling(
hook,
instance,
ErrorCodes.TRANSITION_HOOK,
args
)
}
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.
const child = children[0]
if (isLeaving) {
return placeholder(child)
}
let delayedLeave: (() => void) | undefined
const performDelayedLeave = () => delayedLeave && delayedLeave()
2019-11-23 06:10:17 +08:00
const transitionHooks = (child.transition = resolveTransitionHooks(
rawProps,
2019-11-23 06:10:17 +08:00
callTransitionHook,
isMounted,
2019-11-23 06:10:17 +08:00
pendingCallbacks,
performDelayedLeave
))
// clone old subTree because we need to modify it
const oldChild = instance.subTree
? (instance.subTree = cloneVNode(instance.subTree))
: null
// handle mode
if (
oldChild &&
!isSameVNodeType(child, oldChild) &&
oldChild.type !== Comment
) {
// update old tree's hooks in case of dynamic transition
// need to do this recursively in case of HOCs
2019-11-23 06:10:17 +08:00
updateHOCTransitionData(oldChild, transitionHooks)
// 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 = () => {
isLeaving = false
instance.update()
}
return placeholder(child)
} else if (mode === 'in-out') {
2019-11-23 06:10:17 +08:00
transitionHooks.delayLeave = performLeave => {
delayedLeave = performLeave
}
}
}
return child
}
}
2019-11-21 23:21:09 +08:00
}
if (__DEV__) {
2019-11-21 23:21:09 +08:00
;(TransitionImpl 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
}
}
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 {
persisted: boolean
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(
{
appear,
persisted = false,
onBeforeEnter,
onEnter,
onAfterEnter,
onEnterCancelled,
onBeforeLeave,
onLeave,
onAfterLeave,
onLeaveCancelled
}: TransitionProps,
2019-11-23 06:10:17 +08:00
callHook: TransitionHookCaller,
isMounted: boolean,
2019-11-23 06:10:17 +08:00
pendingCallbacks: PendingCallbacks,
performDelayedLeave: () => void
2019-11-23 06:10:17 +08:00
): TransitionHooks {
return {
persisted,
beforeEnter(el) {
if (!isMounted && !appear) {
return
}
2019-11-23 06:10:17 +08:00
if (pendingCallbacks.leave) {
pendingCallbacks.leave(true /* cancelled */)
}
callHook(onBeforeEnter, [el])
},
2019-11-23 06:10:17 +08:00
enter(el) {
if (!isMounted && !appear) {
return
}
let called = false
2019-11-23 06:10:17 +08:00
const afterEnter = (pendingCallbacks.enter = (cancelled?) => {
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
})
if (onEnter) {
2019-11-23 06:10:17 +08:00
onEnter(el, afterEnter)
} else {
2019-11-23 06:10:17 +08:00
afterEnter()
}
},
2019-11-23 06:10:17 +08:00
leave(el, remove) {
2019-11-23 06:10:17 +08:00
if (pendingCallbacks.enter) {
pendingCallbacks.enter(true /* cancelled */)
}
callHook(onBeforeLeave, [el])
let called = false
2019-11-23 06:10:17 +08:00
const afterLeave = (pendingCallbacks.leave = (cancelled?) => {
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
})
if (onLeave) {
2019-11-23 06:10:17 +08:00
onLeave(el, afterLeave)
} else {
afterLeave()
}
}
}
}
// 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-23 06:10:17 +08:00
function updateHOCTransitionData(vnode: VNode, data: TransitionHooks) {
if (vnode.shapeFlag & ShapeFlags.COMPONENT) {
updateHOCTransitionData(vnode.component!.subTree, data)
} else {
vnode.transition = data
}
}