| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380 |
- import {
- type ElementWithTransition,
- type TransitionGroupProps,
- type TransitionProps,
- TransitionPropsValidators,
- type TransitionState,
- baseApplyTranslation,
- callPendingCbs,
- currentInstance,
- forceReflow,
- handleMovedChildren,
- hasCSSTransform,
- onBeforeUpdate,
- onUpdated,
- resolveTransitionProps,
- useTransitionState,
- warn,
- } from '@vue/runtime-dom'
- import { extend, isArray } from '@vue/shared'
- import {
- type Block,
- type BlockFn,
- type TransitionBlock,
- insert,
- } from '../block'
- import { renderEffect } from '../renderEffect'
- import {
- type ResolvedTransitionBlock,
- ensureTransitionHooksRegistered,
- getTransitionElementFromVNode,
- resolveTransitionHooks,
- setTransitionHooks,
- } from './Transition'
- import {
- type VaporComponentInstance,
- type VaporComponentOptions,
- isVaporComponent,
- } from '../component'
- import { isForBlock, setForHydrationAnchorResolver } from '../apiCreateFor'
- import { createComment, createElement, createTextNode } from '../dom/node'
- import { DynamicFragment, type VaporFragment, isFragment } from '../fragment'
- import {
- type DefineVaporComponent,
- defineVaporComponent,
- } from '../apiDefineComponent'
- import { isInteropEnabled } from '../vdomInteropState'
- import {
- adoptTemplate,
- cleanupHydrationTail,
- currentHydrationNode,
- isHydrating,
- locateNextNode,
- markHydrationAnchor,
- setCurrentHydrationNode,
- } from '../dom/hydration'
- const positionMap = new WeakMap<TransitionBlock, DOMRect>()
- const newPositionMap = new WeakMap<TransitionBlock, DOMRect>()
- let isForHydrationAnchorResolverRegistered = false
- let currentForHydrationContainer: ParentNode | undefined
- function ensureForHydrationAnchorResolver(): void {
- if (isForHydrationAnchorResolverRegistered) return
- isForHydrationAnchorResolverRegistered = true
- setForHydrationAnchorResolver((hydrationStart, anchorNode) => {
- const container = currentForHydrationContainer
- if (!container) return
- if (
- hydrationStart !== container &&
- hydrationStart.parentNode !== container
- ) {
- return
- }
- const anchor =
- anchorNode &&
- anchorNode !== container &&
- anchorNode.parentNode === container
- ? anchorNode
- : null
- const parentAnchor = markHydrationAnchor(
- __DEV__ ? createComment('for') : createTextNode(),
- )
- container.insertBefore(parentAnchor, anchor)
- return parentAnchor
- })
- }
- const decorate = <T extends VaporComponentOptions>(t: T): T => {
- delete (t.props! as any).mode
- return t
- }
- const VaporTransitionGroupImpl = defineVaporComponent({
- name: 'VaporTransitionGroup',
- props: /*@__PURE__*/ extend({}, TransitionPropsValidators, {
- tag: String,
- moveClass: String,
- }),
- setup(props: TransitionGroupProps, { slots, expose }) {
- // @ts-expect-error
- expose()
- // Register transition hooks on first use
- ensureTransitionHooksRegistered()
- const instance = currentInstance as VaporComponentInstance
- const state = useTransitionState()
- // use proxy to keep props reference stable
- let cssTransitionProps = resolveTransitionProps(props)
- const propsProxy = new Proxy({} as typeof cssTransitionProps, {
- get(_, key) {
- return cssTransitionProps[key as keyof typeof cssTransitionProps]
- },
- })
- renderEffect(() => {
- cssTransitionProps = resolveTransitionProps(props)
- })
- let prevChildren: ResolvedTransitionBlock[]
- let slottedBlock: Block = []
- onBeforeUpdate(() => {
- prevChildren = []
- const children = getTransitionBlocks(slottedBlock)
- for (let i = 0; i < children.length; i++) {
- const child = children[i]
- const el =
- isValidTransitionBlock(child) && child.$transition
- ? getTransitionElement(child)
- : undefined
- if (el) {
- prevChildren.push(child)
- // disabled transition during enter, so the children will be
- // inserted into the correct position immediately. this prevents
- // `recordPosition` from getting incorrect positions in `onUpdated`
- child.$transition!.disabled = true
- positionMap.set(child, el.getBoundingClientRect())
- }
- }
- })
- onUpdated(() => {
- if (!prevChildren.length) {
- return
- }
- const moveClass = props.moveClass || `${props.name || 'v'}-move`
- const firstChild = getFirstConnectedChild(prevChildren)
- if (
- !firstChild ||
- !hasCSSTransform(
- firstChild as ElementWithTransition,
- firstChild.parentNode as Node,
- moveClass,
- )
- ) {
- prevChildren = []
- return
- }
- prevChildren.forEach(callPendingCbs)
- prevChildren.forEach(child => {
- child.$transition!.disabled = false
- recordPosition(child)
- })
- const movedChildren = prevChildren.filter(applyTranslation)
- // force reflow to put everything in position
- forceReflow()
- movedChildren.forEach(c =>
- handleMovedChildren(
- getTransitionElement(c) as ElementWithTransition,
- moveClass,
- ),
- )
- prevChildren = []
- })
- const frag = new DynamicFragment('transition-group')
- let currentTag: string | undefined
- let currentSlot: BlockFn | undefined
- let isMounted = false
- renderEffect(() => {
- const tag = props.tag
- const slot = slots.default
- // if the tag and slot are the same as previous render, no need to update.
- if (isMounted && tag === currentTag && slot === currentSlot) return
- const container = tag
- ? isHydrating
- ? (adoptTemplate(currentHydrationNode!, `<${tag}/>`) as HTMLElement)
- : createElement(tag)
- : undefined
- let nextNode: Node | null = null
- let prevForHydrationContainer: ParentNode | undefined
- if (isHydrating && container) {
- // `transition-group + v-for` SSR output does not include `<!--]-->`.
- // Expose the container so `v-for` hydration can create its own anchor.
- ensureForHydrationAnchorResolver()
- prevForHydrationContainer = currentForHydrationContainer
- currentForHydrationContainer = container
- nextNode = locateNextNode(container)
- setCurrentHydrationNode(container.firstChild || container)
- }
- let block: Block = slottedBlock
- let transitionBlocks: ResolvedTransitionBlock[] = []
- try {
- frag.update(() => {
- block = (slot && slot()) || []
- transitionBlocks = applyGroupTransitionHooks(
- block,
- propsProxy,
- state,
- instance,
- )
- if (container) {
- if (!isHydrating) insert(block, container)
- return container
- }
- return block
- })
- if (
- isHydrating &&
- container &&
- currentHydrationNode &&
- currentHydrationNode.parentNode === container &&
- !transitionBlocks.some(child => child === currentHydrationNode)
- ) {
- // Remove extra SSR nodes left after hydrating the current children,
- // but keep a node that was claimed as a transition child.
- cleanupHydrationTail(currentHydrationNode, container)
- }
- } finally {
- if (isHydrating && container) {
- currentForHydrationContainer = prevForHydrationContainer
- setCurrentHydrationNode(nextNode)
- }
- }
- slottedBlock = block
- currentTag = tag
- currentSlot = slot
- isMounted = true
- })
- return frag
- },
- })
- export const VaporTransitionGroup: DefineVaporComponent<
- {},
- string,
- TransitionGroupProps
- > = /*@__PURE__*/ decorate(VaporTransitionGroupImpl)
- function applyGroupTransitionHooks(
- block: Block,
- props: TransitionProps,
- state: TransitionState,
- instance: VaporComponentInstance,
- ): ResolvedTransitionBlock[] {
- const fragments: VaporFragment[] = []
- const children = getTransitionBlocks(block, frag => fragments.push(frag))
- for (let i = 0; i < children.length; i++) {
- const child = children[i]
- if (isValidTransitionBlock(child)) {
- if (child.$key != null) {
- setTransitionHooks(
- child,
- resolveTransitionHooks(child, props, state, instance),
- )
- } else if (__DEV__) {
- warn(`<transition-group> children must be keyed`)
- }
- }
- }
- // propagate hooks to inner fragments for reusing during insert new items
- fragments.forEach(frag => {
- const hooks = resolveTransitionHooks(frag, props, state, instance)
- hooks.applyGroup = applyGroupTransitionHooks
- frag.$transition = hooks
- })
- return children
- }
- function inheritKey(children: TransitionBlock[], key: any): void {
- if (key === undefined || children.length === 0) return
- for (let i = 0; i < children.length; i++) {
- const child = children[i]
- child.$key = String(key) + String(child.$key != null ? child.$key : i)
- }
- }
- function getTransitionBlocks(
- block: Block,
- onFragment?: (frag: VaporFragment) => void,
- ): ResolvedTransitionBlock[] {
- let children: ResolvedTransitionBlock[] = []
- if (block instanceof Element) {
- children.push(block)
- } else if (isVaporComponent(block)) {
- const blocks = getTransitionBlocks(block.block, onFragment)
- inheritKey(blocks, block.$key)
- children.push(...blocks)
- } else if (isArray(block)) {
- for (let i = 0; i < block.length; i++) {
- const b = block[i]
- const blocks = getTransitionBlocks(b, onFragment)
- if (isForBlock(b)) blocks.forEach(block => (block.$key = b.key))
- children.push(...blocks)
- }
- } else if (isFragment(block)) {
- if (isInteropEnabled && block.vnode) {
- // vdom component
- children.push(block)
- } else {
- if (onFragment) onFragment(block)
- const blocks = getTransitionBlocks(block.nodes, onFragment)
- inheritKey(blocks, block.$key)
- children.push(...blocks)
- }
- }
- return children
- }
- function isValidTransitionBlock(
- block: Block,
- ): block is ResolvedTransitionBlock {
- return !!(block instanceof Element || (isFragment(block) && block.vnode))
- }
- function getTransitionElement(
- block: ResolvedTransitionBlock,
- ): Element | undefined {
- if (block instanceof Element) return block
- // vdom interop
- if (isInteropEnabled && isFragment(block) && block.vnode) {
- return getTransitionElementFromVNode(block.vnode)
- }
- }
- function recordPosition(c: ResolvedTransitionBlock) {
- const el = getTransitionElement(c)
- if (el) newPositionMap.set(c, el.getBoundingClientRect())
- }
- function applyTranslation(
- c: ResolvedTransitionBlock,
- ): ResolvedTransitionBlock | undefined {
- const el = getTransitionElement(c)
- if (
- el &&
- baseApplyTranslation(
- positionMap.get(c)!,
- newPositionMap.get(c)!,
- el as ElementWithTransition,
- )
- ) {
- return c
- }
- }
- function getFirstConnectedChild(
- children: ResolvedTransitionBlock[],
- ): Element | undefined {
- for (let i = 0; i < children.length; i++) {
- const child = children[i]
- const el = getTransitionElement(child)
- if (el && el.isConnected) return el
- }
- }
|