import { TransitionProps, addTransitionClass, removeTransitionClass, ElementWithTransition, getTransitionInfo, resolveTransitionProps, TransitionPropsValidators } from './Transition' import { Fragment, Comment, VNode, warn, resolveTransitionHooks, useTransitionState, getCurrentInstance, setTransitionHooks, createVNode, onUpdated, SetupContext } from '@vue/runtime-core' import { toRaw } from '@vue/reactivity' interface Position { top: number left: number } const positionMap = new WeakMap() const newPositionMap = new WeakMap() export type TransitionGroupProps = Omit & { tag?: string moveClass?: string } const TransitionGroupImpl = { props: { ...TransitionPropsValidators, tag: String, moveClass: String }, setup(props: TransitionGroupProps, { slots }: SetupContext) { 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 as ElementWithTransition, instance.vnode.el as Node, 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 as ElementWithTransition const style = el.style addTransitionClass(el, moveClass) style.transform = style.webkitTransform = style.transitionDuration = '' const cb = ((el as any)._moveCb = (e: TransitionEvent) => { if (e && e.target !== el) { return } if (!e || /transform$/.test(e.propertyName)) { el.removeEventListener('transitionend', cb) ;(el as any)._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 const slotChildren = slots.default ? slots.default() : [] children = getTransitionRawChildren(slotChildren) 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(` 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 as Element).getBoundingClientRect()) } } return createVNode(tag, null, slotChildren) } } } function getTransitionRawChildren(children: VNode[]): VNode[] { let ret: VNode[] = [] for (let i = 0; i < children.length; i++) { const child = children[i] // handle fragment children case, e.g. v-for if (child.type === Fragment) { ret = ret.concat(getTransitionRawChildren(child.children as VNode[])) } // comment placeholders should be skipped, e.g. v-if else if (child.type !== Comment) { ret.push(child) } } return ret } // remove mode props as TransitionGroup doesn't support it delete TransitionGroupImpl.props.mode export const TransitionGroup = (TransitionGroupImpl as unknown) as { new (): { $props: TransitionGroupProps } } function callPendingCbs(c: VNode) { const el = c.el as any if (el._moveCb) { el._moveCb() } if (el._enterCb) { el._enterCb() } } function recordPosition(c: VNode) { newPositionMap.set(c, (c.el as Element).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 as HTMLElement).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 => { cls.split(/\s+/).forEach(c => c && clone.classList.remove(c)) }) } moveClass.split(/\s+/).forEach(c => c && clone.classList.add(c)) 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 }