import {
getCurrentInstance,
SetupContext,
- ComponentOptions
+ ComponentOptions,
+ ComponentInternalInstance
} from '../component'
import {
cloneVNode,
args?: any[]
) => void
-type PendingCallback = (cancelled?: boolean) => void
+export type PendingCallback = (cancelled?: boolean) => void
-interface TransitionState {
+export interface TransitionState {
isMounted: boolean
isLeaving: boolean
isUnmounting: boolean
leavingVNodes: Map<any, Record<string, VNode>>
}
-interface TransitionElement {
+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.
_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: TransitionState = {
- isMounted: false,
- isLeaving: false,
- isUnmounting: false,
- leavingVNodes: new Map()
- }
- onMounted(() => {
- state.isMounted = true
- })
- onBeforeUnmount(() => {
- state.isUnmounting = true
- })
-
- const callTransitionHook: TransitionHookCaller = (hook, args) => {
- hook &&
- callWithAsyncErrorHandling(
- hook,
- instance,
- ErrorCodes.TRANSITION_HOOK,
- args
- )
- }
+ const state = useTransitionState()
return () => {
const children = slots.default && slots.default()
innerChild,
rawProps,
state,
- callTransitionHook
+ instance
))
const oldChild = instance.subTree
oldInnerChild,
rawProps,
state,
- callTransitionHook
+ instance
)
// update old tree's hooks in case of dynamic transition
setTransitionHooks(oldInnerChild, leavingHooks)
// The transition hooks are attached to the vnode as vnode.transition
// and will be called at appropriate timing in the renderer.
-function resolveTransitionHooks(
+export function resolveTransitionHooks(
vnode: VNode,
{
appear,
onLeaveCancelled
}: BaseTransitionProps,
state: TransitionState,
- callHook: TransitionHookCaller
+ 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) {
callWithErrorHandling,
callWithAsyncErrorHandling
} from './errorHandling'
+export {
+ useTransitionState,
+ TransitionState,
+ resolveTransitionHooks,
+ setTransitionHooks,
+ TransitionHooks
+} from './components/BaseTransition'
// Internal, for compiler generated code
// should sync with '@vue/compiler-core/src/runtimeConstants.ts'
}
}
-function resolveTransitionProps({
+export function resolveTransitionProps({
name = 'v',
type,
css = true,
enterToClass = appearToClass
}
- type Hook = (el: Element, done?: () => void) => void
+ type Hook = (el: HTMLElement, done?: () => void) => void
const finishEnter: Hook = (el, done) => {
removeTransitionClass(el, enterToClass)
}
}
-export interface ElementWithTransition extends Element {
+export interface ElementWithTransition extends HTMLElement {
// _vtc = Vue Transition Classes.
// Store the temporarily-added transition classes on the element
// so that we can avoid overwriting them if the element's class is patched
_vtc?: Set<string>
}
-function addTransitionClass(el: ElementWithTransition, cls: string) {
+export function addTransitionClass(el: ElementWithTransition, cls: string) {
el.classList.add(cls)
;(el._vtc || (el._vtc = new Set())).add(cls)
}
-function removeTransitionClass(el: ElementWithTransition, cls: string) {
+export function removeTransitionClass(el: ElementWithTransition, cls: string) {
el.classList.remove(cls)
if (el._vtc) {
el._vtc.delete(cls)
type: typeof TRANSITION | typeof ANIMATION | null
propCount: number
timeout: number
+ hasTransform: boolean
}
-function getTransitionInfo(
+export function getTransitionInfo(
el: Element,
expectedType?: TransitionProps['type']
): CSSTransitionInfo {
: animationDurations.length
: 0
}
+ const hasTransform =
+ type === TRANSITION &&
+ /\b(transform|all)(,|$)/.test(styles[TRANSITION + 'Property'])
return {
type,
timeout,
- propCount
+ propCount,
+ hasTransform
}
}
--- /dev/null
+import {
+ TransitionProps,
+ addTransitionClass,
+ removeTransitionClass,
+ ElementWithTransition,
+ getTransitionInfo,
+ resolveTransitionProps
+} from './Transition'
+import {
+ Fragment,
+ VNode,
+ Slots,
+ warn,
+ resolveTransitionHooks,
+ toRaw,
+ useTransitionState,
+ getCurrentInstance,
+ setTransitionHooks,
+ createVNode,
+ onUpdated
+} from '@vue/runtime-core'
+
+interface Position {
+ top: number
+ left: number
+}
+
+const positionMap = new WeakMap<VNode, Position>()
+const newPositionMap = new WeakMap<VNode, Position>()
+
+export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
+ tag?: string
+ moveClass?: string
+}
+
+export const TransitionGroup = {
+ setup(props: TransitionGroupProps, { slots }: { slots: Slots }) {
+ const instance = getCurrentInstance()!
+ const state = useTransitionState()
+ let prevChildren: VNode[]
+ let children: VNode[]
+ let hasMove: boolean | null = null
+
+ onUpdated(() => {
+ // children is guaranteed to exist after initial render
+ if (!prevChildren.length) {
+ return
+ }
+ const moveClass = props.moveClass || `${props.name || 'v'}-move`
+ // Check if move transition is needed. This check is cached per-instance.
+ hasMove =
+ hasMove === null
+ ? (hasMove = hasCSSTransform(
+ prevChildren[0].el,
+ instance.vnode.el,
+ moveClass
+ ))
+ : hasMove
+ if (!hasMove) {
+ return
+ }
+
+ // we divide the work into three loops to avoid mixing DOM reads and writes
+ // in each iteration - which helps prevent layout thrashing.
+ prevChildren.forEach(callPendingCbs)
+ prevChildren.forEach(recordPosition)
+ const movedChildren = prevChildren.filter(applyTranslation)
+
+ // force reflow to put everything in position
+ forceReflow()
+
+ movedChildren.forEach(c => {
+ const el = c.el
+ const style = el.style
+ addTransitionClass(el, moveClass)
+ style.transform = style.WebkitTransform = style.transitionDuration = ''
+ const cb = (el._moveCb = (e: TransitionEvent) => {
+ if (e && e.target !== el) {
+ return
+ }
+ if (!e || /transform$/.test(e.propertyName)) {
+ el.removeEventListener('transitionend', cb)
+ el._moveCb = null
+ removeTransitionClass(el, moveClass)
+ }
+ })
+ el.addEventListener('transitionend', cb)
+ })
+ })
+
+ return () => {
+ const rawProps = toRaw(props)
+ const cssTransitionProps = resolveTransitionProps(rawProps)
+ const tag = rawProps.tag || Fragment
+ prevChildren = children
+ children = slots.default ? slots.default() : []
+
+ for (let i = 0; i < children.length; i++) {
+ const child = children[i]
+ if (child.key != null) {
+ setTransitionHooks(
+ child,
+ resolveTransitionHooks(child, cssTransitionProps, state, instance)
+ )
+ } else if (__DEV__) {
+ warn(`<TransitionGroup> children must be keyed.`)
+ }
+ }
+
+ if (prevChildren) {
+ for (let i = 0; i < prevChildren.length; i++) {
+ const child = prevChildren[i]
+ setTransitionHooks(
+ child,
+ resolveTransitionHooks(child, cssTransitionProps, state, instance)
+ )
+ positionMap.set(child, child.el.getBoundingClientRect())
+ }
+ }
+
+ return createVNode(tag, null, children)
+ }
+ }
+}
+
+function callPendingCbs(c: VNode) {
+ if (c.el._moveCb) {
+ c.el._moveCb()
+ }
+ if (c.el._enterCb) {
+ c.el._enterCb()
+ }
+}
+
+function recordPosition(c: VNode) {
+ newPositionMap.set(c, c.el.getBoundingClientRect())
+}
+
+function applyTranslation(c: VNode): VNode | undefined {
+ const oldPos = positionMap.get(c)!
+ const newPos = newPositionMap.get(c)!
+ const dx = oldPos.left - newPos.left
+ const dy = oldPos.top - newPos.top
+ if (dx || dy) {
+ const s = c.el.style
+ s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
+ s.transitionDuration = '0s'
+ return c
+ }
+}
+
+// this is put in a dedicated function to avoid the line from being treeshaken
+function forceReflow() {
+ return document.body.offsetHeight
+}
+
+function hasCSSTransform(
+ el: ElementWithTransition,
+ root: Node,
+ moveClass: string
+): boolean {
+ // Detect whether an element with the move class applied has
+ // CSS transitions. Since the element may be inside an entering
+ // transition at this very moment, we make a clone of it and remove
+ // all other transition classes applied to ensure only the move class
+ // is applied.
+ const clone = el.cloneNode() as HTMLElement
+ if (el._vtc) {
+ el._vtc.forEach(cls => clone.classList.remove(cls))
+ }
+ clone.classList.add(moveClass)
+ clone.style.display = 'none'
+ const container = (root.nodeType === 1
+ ? root
+ : root.parentNode) as HTMLElement
+ container.appendChild(clone)
+ const { hasTransform } = getTransitionInfo(clone)
+ container.removeChild(clone)
+ return hasTransform
+}
return app
}
-// DOM-only runtime helpers
+// DOM-only runtime directive helpers
export {
vModelText,
vModelCheckbox,
vModelDynamic
} from './directives/vModel'
export { withModifiers, withKeys } from './directives/vOn'
+export { vShow } from './directives/vShow'
// DOM-only components
export { Transition, TransitionProps } from './components/Transition'
-
-export { vShow } from './directives/vShow'
+export {
+ TransitionGroup,
+ TransitionGroupProps
+} from './components/TransitionGroup'
// re-export everything from core
// h, Component, reactivity API, nextTick, flags & types