206 lines
5.7 KiB
TypeScript
206 lines
5.7 KiB
TypeScript
import {
|
|
TransitionProps,
|
|
addTransitionClass,
|
|
removeTransitionClass,
|
|
ElementWithTransition,
|
|
getTransitionInfo,
|
|
resolveTransitionProps,
|
|
TransitionPropsValidators
|
|
} from './Transition'
|
|
import {
|
|
Fragment,
|
|
VNode,
|
|
warn,
|
|
resolveTransitionHooks,
|
|
useTransitionState,
|
|
getTransitionRawChildren,
|
|
getCurrentInstance,
|
|
setTransitionHooks,
|
|
createVNode,
|
|
onUpdated,
|
|
SetupContext
|
|
} from '@vue/runtime-core'
|
|
import { toRaw } from '@vue/reactivity'
|
|
import { extend } from '@vue/shared'
|
|
|
|
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
|
|
}
|
|
|
|
const TransitionGroupImpl = {
|
|
name: 'TransitionGroup',
|
|
|
|
props: /*#__PURE__*/ extend({}, TransitionPropsValidators, {
|
|
tag: String,
|
|
moveClass: String
|
|
}),
|
|
|
|
setup(props: TransitionGroupProps, { slots }: SetupContext) {
|
|
const instance = getCurrentInstance()!
|
|
const state = useTransitionState()
|
|
let prevChildren: VNode[]
|
|
let children: VNode[]
|
|
|
|
onUpdated(() => {
|
|
// children is guaranteed to exist after initial render
|
|
if (!prevChildren.length) {
|
|
return
|
|
}
|
|
const moveClass = props.moveClass || `${props.name || 'v'}-move`
|
|
|
|
if (
|
|
!hasCSSTransform(
|
|
prevChildren[0].el as ElementWithTransition,
|
|
instance.vnode.el as Node,
|
|
moveClass
|
|
)
|
|
) {
|
|
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
|
|
children = slots.default ? getTransitionRawChildren(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 as Element).getBoundingClientRect())
|
|
}
|
|
}
|
|
|
|
return createVNode(tag, null, children)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* TransitionGroup does not support "mode" so we need to remove it from the
|
|
* props declarations, but direct delete operation is considered a side effect
|
|
* and will make the entire transition feature non-tree-shakeable, so we do it
|
|
* in a function and mark the function's invocation as pure.
|
|
*/
|
|
const removeMode = (props: any) => delete props.mode
|
|
/*#__PURE__*/ removeMode(TransitionGroupImpl.props)
|
|
|
|
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
|
|
}
|