2025-03-11 21:37:33 +08:00
|
|
|
import {
|
|
|
|
type ElementWithTransition,
|
|
|
|
type TransitionGroupProps,
|
|
|
|
TransitionPropsValidators,
|
|
|
|
baseApplyTranslation,
|
|
|
|
callPendingCbs,
|
|
|
|
currentInstance,
|
|
|
|
forceReflow,
|
|
|
|
handleMovedChildren,
|
|
|
|
hasCSSTransform,
|
|
|
|
onBeforeUpdate,
|
|
|
|
onUpdated,
|
|
|
|
resolveTransitionProps,
|
|
|
|
useTransitionState,
|
|
|
|
warn,
|
|
|
|
} from '@vue/runtime-dom'
|
|
|
|
import { extend, isArray } from '@vue/shared'
|
|
|
|
import {
|
|
|
|
type Block,
|
|
|
|
DynamicFragment,
|
|
|
|
type TransitionBlock,
|
|
|
|
type VaporTransitionHooks,
|
|
|
|
insert,
|
|
|
|
isFragment,
|
|
|
|
} from '../block'
|
|
|
|
import {
|
|
|
|
resolveTransitionHooks,
|
|
|
|
setTransitionHooks,
|
|
|
|
setTransitionHooksToFragment,
|
|
|
|
} from './Transition'
|
2025-03-13 21:46:40 +08:00
|
|
|
import {
|
|
|
|
type ObjectVaporComponent,
|
|
|
|
type VaporComponentInstance,
|
|
|
|
isVaporComponent,
|
|
|
|
} from '../component'
|
2025-03-11 21:37:33 +08:00
|
|
|
import { isForBlock } from '../apiCreateFor'
|
|
|
|
import { renderEffect, setDynamicProps } from '@vue/runtime-vapor'
|
|
|
|
|
|
|
|
const positionMap = new WeakMap<TransitionBlock, DOMRect>()
|
|
|
|
const newPositionMap = new WeakMap<TransitionBlock, DOMRect>()
|
|
|
|
|
|
|
|
const decorate = (t: typeof VaporTransitionGroup) => {
|
|
|
|
delete (t.props! as any).mode
|
|
|
|
t.__vapor = true
|
|
|
|
return t
|
|
|
|
}
|
|
|
|
|
|
|
|
export const VaporTransitionGroup: ObjectVaporComponent = decorate({
|
|
|
|
name: 'VaporTransitionGroup',
|
|
|
|
|
|
|
|
props: /*@__PURE__*/ extend({}, TransitionPropsValidators, {
|
|
|
|
tag: String,
|
|
|
|
moveClass: String,
|
|
|
|
}),
|
|
|
|
|
2025-03-13 17:35:25 +08:00
|
|
|
setup(props: TransitionGroupProps, { slots }) {
|
2025-03-13 21:46:40 +08:00
|
|
|
const instance = currentInstance as VaporComponentInstance
|
2025-03-11 21:37:33 +08:00
|
|
|
const state = useTransitionState()
|
|
|
|
const cssTransitionProps = resolveTransitionProps(props)
|
|
|
|
|
|
|
|
let prevChildren: TransitionBlock[]
|
|
|
|
let children: TransitionBlock[]
|
|
|
|
let slottedBlock: Block
|
|
|
|
|
|
|
|
onBeforeUpdate(() => {
|
|
|
|
prevChildren = []
|
|
|
|
children = getTransitionBlocks(slottedBlock)
|
|
|
|
if (children) {
|
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
|
|
const child = children[i]
|
|
|
|
if (isValidTransitionBlock(child)) {
|
|
|
|
prevChildren.push(child)
|
2025-03-12 11:47:20 +08:00
|
|
|
// disabled transition during enter, so the children will be
|
2025-03-11 21:37:33 +08:00
|
|
|
// inserted into the correct position immediately. this prevents
|
|
|
|
// `recordPosition` from getting incorrect positions in `onUpdated`
|
2025-03-12 11:47:20 +08:00
|
|
|
child.$transition!.disabled = true
|
2025-03-12 09:12:39 +08:00
|
|
|
positionMap.set(
|
|
|
|
child,
|
|
|
|
getTransitionElement(child).getBoundingClientRect(),
|
|
|
|
)
|
2025-03-11 21:37:33 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
onUpdated(() => {
|
|
|
|
if (!prevChildren.length) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
const moveClass = props.moveClass || `${props.name || 'v'}-move`
|
|
|
|
|
2025-03-12 09:12:39 +08:00
|
|
|
const firstChild = getFirstConnectedChild(prevChildren)
|
2025-03-11 21:37:33 +08:00
|
|
|
if (
|
|
|
|
!firstChild ||
|
|
|
|
!hasCSSTransform(
|
|
|
|
firstChild as ElementWithTransition,
|
|
|
|
firstChild.parentNode as Node,
|
|
|
|
moveClass,
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
prevChildren.forEach(callPendingCbs)
|
|
|
|
prevChildren.forEach(child => {
|
2025-03-12 11:47:20 +08:00
|
|
|
delete child.$transition!.disabled
|
2025-03-11 21:37:33 +08:00
|
|
|
recordPosition(child)
|
|
|
|
})
|
|
|
|
const movedChildren = prevChildren.filter(applyTranslation)
|
|
|
|
|
|
|
|
// force reflow to put everything in position
|
|
|
|
forceReflow()
|
|
|
|
|
|
|
|
movedChildren.forEach(c =>
|
2025-03-12 09:12:39 +08:00
|
|
|
handleMovedChildren(
|
|
|
|
getTransitionElement(c) as ElementWithTransition,
|
|
|
|
moveClass,
|
|
|
|
),
|
2025-03-11 21:37:33 +08:00
|
|
|
)
|
|
|
|
})
|
|
|
|
|
|
|
|
slottedBlock = slots.default && slots.default()
|
|
|
|
|
|
|
|
// store props and state on fragment for reusing during insert new items
|
|
|
|
setTransitionHooksToFragment(slottedBlock, {
|
|
|
|
props: cssTransitionProps,
|
|
|
|
state,
|
|
|
|
} as VaporTransitionHooks)
|
|
|
|
|
|
|
|
children = getTransitionBlocks(slottedBlock)
|
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
|
|
const child = children[i]
|
|
|
|
if (isValidTransitionBlock(child)) {
|
2025-03-12 11:47:20 +08:00
|
|
|
if (child.$key != null) {
|
2025-03-11 21:37:33 +08:00
|
|
|
setTransitionHooks(
|
|
|
|
child,
|
|
|
|
resolveTransitionHooks(child, cssTransitionProps, state, instance!),
|
|
|
|
)
|
2025-03-12 11:47:20 +08:00
|
|
|
} else if (__DEV__ && child.$key == null) {
|
2025-03-11 21:37:33 +08:00
|
|
|
warn(`<transition-group> children must be keyed`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const tag = props.tag
|
|
|
|
if (tag) {
|
2025-03-12 11:09:46 +08:00
|
|
|
const container = document.createElement(tag)
|
|
|
|
insert(slottedBlock, container)
|
2025-03-11 21:37:33 +08:00
|
|
|
// fallthrough attrs
|
2025-03-13 21:46:40 +08:00
|
|
|
if (instance!.hasFallthrough) {
|
|
|
|
renderEffect(() => setDynamicProps(container, [instance!.attrs]))
|
|
|
|
}
|
2025-03-12 11:09:46 +08:00
|
|
|
return container
|
2025-03-11 21:37:33 +08:00
|
|
|
} else {
|
|
|
|
const frag = __DEV__
|
|
|
|
? new DynamicFragment('transitionGroup')
|
|
|
|
: new DynamicFragment()
|
|
|
|
renderEffect(() => frag.update(() => slottedBlock))
|
|
|
|
return frag
|
|
|
|
}
|
|
|
|
},
|
|
|
|
})
|
|
|
|
|
|
|
|
function getTransitionBlocks(block: Block) {
|
|
|
|
let children: TransitionBlock[] = []
|
|
|
|
if (block instanceof Node) {
|
|
|
|
children.push(block)
|
|
|
|
} else if (isVaporComponent(block)) {
|
|
|
|
children.push(...getTransitionBlocks(block.block))
|
|
|
|
} else if (isArray(block)) {
|
|
|
|
for (let i = 0; i < block.length; i++) {
|
|
|
|
const b = block[i]
|
|
|
|
const blocks = getTransitionBlocks(b)
|
|
|
|
if (isForBlock(b)) blocks.forEach(block => (block.$key = b.key))
|
|
|
|
children.push(...blocks)
|
|
|
|
}
|
|
|
|
} else if (isFragment(block)) {
|
|
|
|
if (block.insert) {
|
|
|
|
// vdom component
|
|
|
|
children.push(block)
|
|
|
|
} else {
|
|
|
|
children.push(...getTransitionBlocks(block.nodes))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return children
|
|
|
|
}
|
|
|
|
|
|
|
|
function isValidTransitionBlock(block: Block): boolean {
|
|
|
|
return !!(block instanceof Element || (isFragment(block) && block.insert))
|
|
|
|
}
|
|
|
|
|
2025-03-12 09:12:39 +08:00
|
|
|
function getTransitionElement(c: TransitionBlock): Element {
|
|
|
|
return (isFragment(c) ? (c.nodes as Element[])[0] : c) as Element
|
2025-03-11 21:37:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
function recordPosition(c: TransitionBlock) {
|
2025-03-12 09:12:39 +08:00
|
|
|
newPositionMap.set(c, getTransitionElement(c).getBoundingClientRect())
|
2025-03-11 21:37:33 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
function applyTranslation(c: TransitionBlock): TransitionBlock | undefined {
|
|
|
|
if (
|
|
|
|
baseApplyTranslation(
|
|
|
|
positionMap.get(c)!,
|
|
|
|
newPositionMap.get(c)!,
|
2025-03-12 09:12:39 +08:00
|
|
|
getTransitionElement(c) as ElementWithTransition,
|
2025-03-11 21:37:33 +08:00
|
|
|
)
|
|
|
|
) {
|
|
|
|
return c
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-03-12 09:12:39 +08:00
|
|
|
function getFirstConnectedChild(
|
|
|
|
children: TransitionBlock[],
|
|
|
|
): Element | undefined {
|
2025-03-11 21:37:33 +08:00
|
|
|
for (let i = 0; i < children.length; i++) {
|
|
|
|
const child = children[i]
|
2025-03-12 09:12:39 +08:00
|
|
|
const el = getTransitionElement(child)
|
2025-03-11 21:37:33 +08:00
|
|
|
if (el.isConnected) return el
|
|
|
|
}
|
|
|
|
}
|