TransitionGroup.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. import {
  2. type ElementWithTransition,
  3. type TransitionGroupProps,
  4. type TransitionProps,
  5. TransitionPropsValidators,
  6. type TransitionState,
  7. baseApplyTranslation,
  8. callPendingCbs,
  9. currentInstance,
  10. forceReflow,
  11. handleMovedChildren,
  12. hasCSSTransform,
  13. onBeforeUpdate,
  14. onUpdated,
  15. resolveTransitionProps,
  16. useTransitionState,
  17. warn,
  18. } from '@vue/runtime-dom'
  19. import { extend, isArray } from '@vue/shared'
  20. import {
  21. type Block,
  22. type BlockFn,
  23. type TransitionBlock,
  24. insert,
  25. } from '../block'
  26. import { renderEffect } from '../renderEffect'
  27. import {
  28. type ResolvedTransitionBlock,
  29. ensureTransitionHooksRegistered,
  30. getTransitionElementFromVNode,
  31. resolveTransitionHooks,
  32. setTransitionHooks,
  33. } from './Transition'
  34. import {
  35. type VaporComponentInstance,
  36. type VaporComponentOptions,
  37. isVaporComponent,
  38. } from '../component'
  39. import { isForBlock, setForHydrationAnchorResolver } from '../apiCreateFor'
  40. import { createComment, createElement, createTextNode } from '../dom/node'
  41. import { DynamicFragment, type VaporFragment, isFragment } from '../fragment'
  42. import {
  43. type DefineVaporComponent,
  44. defineVaporComponent,
  45. } from '../apiDefineComponent'
  46. import { isInteropEnabled } from '../vdomInteropState'
  47. import {
  48. adoptTemplate,
  49. cleanupHydrationTail,
  50. currentHydrationNode,
  51. isHydrating,
  52. locateNextNode,
  53. markHydrationAnchor,
  54. setCurrentHydrationNode,
  55. } from '../dom/hydration'
  56. const positionMap = new WeakMap<TransitionBlock, DOMRect>()
  57. const newPositionMap = new WeakMap<TransitionBlock, DOMRect>()
  58. let isForHydrationAnchorResolverRegistered = false
  59. let currentForHydrationContainer: ParentNode | undefined
  60. function ensureForHydrationAnchorResolver(): void {
  61. if (isForHydrationAnchorResolverRegistered) return
  62. isForHydrationAnchorResolverRegistered = true
  63. setForHydrationAnchorResolver((hydrationStart, anchorNode) => {
  64. const container = currentForHydrationContainer
  65. if (!container) return
  66. if (
  67. hydrationStart !== container &&
  68. hydrationStart.parentNode !== container
  69. ) {
  70. return
  71. }
  72. const anchor =
  73. anchorNode &&
  74. anchorNode !== container &&
  75. anchorNode.parentNode === container
  76. ? anchorNode
  77. : null
  78. const parentAnchor = markHydrationAnchor(
  79. __DEV__ ? createComment('for') : createTextNode(),
  80. )
  81. container.insertBefore(parentAnchor, anchor)
  82. return parentAnchor
  83. })
  84. }
  85. const decorate = <T extends VaporComponentOptions>(t: T): T => {
  86. delete (t.props! as any).mode
  87. return t
  88. }
  89. const VaporTransitionGroupImpl = defineVaporComponent({
  90. name: 'VaporTransitionGroup',
  91. props: /*@__PURE__*/ extend({}, TransitionPropsValidators, {
  92. tag: String,
  93. moveClass: String,
  94. }),
  95. setup(props: TransitionGroupProps, { slots, expose }) {
  96. // @ts-expect-error
  97. expose()
  98. // Register transition hooks on first use
  99. ensureTransitionHooksRegistered()
  100. const instance = currentInstance as VaporComponentInstance
  101. const state = useTransitionState()
  102. // use proxy to keep props reference stable
  103. let cssTransitionProps = resolveTransitionProps(props)
  104. const propsProxy = new Proxy({} as typeof cssTransitionProps, {
  105. get(_, key) {
  106. return cssTransitionProps[key as keyof typeof cssTransitionProps]
  107. },
  108. })
  109. renderEffect(() => {
  110. cssTransitionProps = resolveTransitionProps(props)
  111. })
  112. let prevChildren: ResolvedTransitionBlock[]
  113. let slottedBlock: Block = []
  114. onBeforeUpdate(() => {
  115. prevChildren = []
  116. const children = getTransitionBlocks(slottedBlock)
  117. for (let i = 0; i < children.length; i++) {
  118. const child = children[i]
  119. const el =
  120. isValidTransitionBlock(child) && child.$transition
  121. ? getTransitionElement(child)
  122. : undefined
  123. if (el) {
  124. prevChildren.push(child)
  125. // disabled transition during enter, so the children will be
  126. // inserted into the correct position immediately. this prevents
  127. // `recordPosition` from getting incorrect positions in `onUpdated`
  128. child.$transition!.disabled = true
  129. positionMap.set(child, el.getBoundingClientRect())
  130. }
  131. }
  132. })
  133. onUpdated(() => {
  134. if (!prevChildren.length) {
  135. return
  136. }
  137. const moveClass = props.moveClass || `${props.name || 'v'}-move`
  138. const firstChild = getFirstConnectedChild(prevChildren)
  139. if (
  140. !firstChild ||
  141. !hasCSSTransform(
  142. firstChild as ElementWithTransition,
  143. firstChild.parentNode as Node,
  144. moveClass,
  145. )
  146. ) {
  147. prevChildren = []
  148. return
  149. }
  150. prevChildren.forEach(callPendingCbs)
  151. prevChildren.forEach(child => {
  152. child.$transition!.disabled = false
  153. recordPosition(child)
  154. })
  155. const movedChildren = prevChildren.filter(applyTranslation)
  156. // force reflow to put everything in position
  157. forceReflow()
  158. movedChildren.forEach(c =>
  159. handleMovedChildren(
  160. getTransitionElement(c) as ElementWithTransition,
  161. moveClass,
  162. ),
  163. )
  164. prevChildren = []
  165. })
  166. const frag = new DynamicFragment('transition-group')
  167. let currentTag: string | undefined
  168. let currentSlot: BlockFn | undefined
  169. let isMounted = false
  170. renderEffect(() => {
  171. const tag = props.tag
  172. const slot = slots.default
  173. // if the tag and slot are the same as previous render, no need to update.
  174. if (isMounted && tag === currentTag && slot === currentSlot) return
  175. const container = tag
  176. ? isHydrating
  177. ? (adoptTemplate(currentHydrationNode!, `<${tag}/>`) as HTMLElement)
  178. : createElement(tag)
  179. : undefined
  180. let nextNode: Node | null = null
  181. let prevForHydrationContainer: ParentNode | undefined
  182. if (isHydrating && container) {
  183. // `transition-group + v-for` SSR output does not include `<!--]-->`.
  184. // Expose the container so `v-for` hydration can create its own anchor.
  185. ensureForHydrationAnchorResolver()
  186. prevForHydrationContainer = currentForHydrationContainer
  187. currentForHydrationContainer = container
  188. nextNode = locateNextNode(container)
  189. setCurrentHydrationNode(container.firstChild || container)
  190. }
  191. let block: Block = slottedBlock
  192. let transitionBlocks: ResolvedTransitionBlock[] = []
  193. try {
  194. frag.update(() => {
  195. block = (slot && slot()) || []
  196. transitionBlocks = applyGroupTransitionHooks(
  197. block,
  198. propsProxy,
  199. state,
  200. instance,
  201. )
  202. if (container) {
  203. if (!isHydrating) insert(block, container)
  204. return container
  205. }
  206. return block
  207. })
  208. if (
  209. isHydrating &&
  210. container &&
  211. currentHydrationNode &&
  212. currentHydrationNode.parentNode === container &&
  213. !transitionBlocks.some(child => child === currentHydrationNode)
  214. ) {
  215. // Remove extra SSR nodes left after hydrating the current children,
  216. // but keep a node that was claimed as a transition child.
  217. cleanupHydrationTail(currentHydrationNode, container)
  218. }
  219. } finally {
  220. if (isHydrating && container) {
  221. currentForHydrationContainer = prevForHydrationContainer
  222. setCurrentHydrationNode(nextNode)
  223. }
  224. }
  225. slottedBlock = block
  226. currentTag = tag
  227. currentSlot = slot
  228. isMounted = true
  229. })
  230. return frag
  231. },
  232. })
  233. export const VaporTransitionGroup: DefineVaporComponent<
  234. {},
  235. string,
  236. TransitionGroupProps
  237. > = /*@__PURE__*/ decorate(VaporTransitionGroupImpl)
  238. function applyGroupTransitionHooks(
  239. block: Block,
  240. props: TransitionProps,
  241. state: TransitionState,
  242. instance: VaporComponentInstance,
  243. ): ResolvedTransitionBlock[] {
  244. const fragments: VaporFragment[] = []
  245. const children = getTransitionBlocks(block, frag => fragments.push(frag))
  246. for (let i = 0; i < children.length; i++) {
  247. const child = children[i]
  248. if (isValidTransitionBlock(child)) {
  249. if (child.$key != null) {
  250. setTransitionHooks(
  251. child,
  252. resolveTransitionHooks(child, props, state, instance),
  253. )
  254. } else if (__DEV__) {
  255. warn(`<transition-group> children must be keyed`)
  256. }
  257. }
  258. }
  259. // propagate hooks to inner fragments for reusing during insert new items
  260. fragments.forEach(frag => {
  261. const hooks = resolveTransitionHooks(frag, props, state, instance)
  262. hooks.applyGroup = applyGroupTransitionHooks
  263. frag.$transition = hooks
  264. })
  265. return children
  266. }
  267. function inheritKey(children: TransitionBlock[], key: any): void {
  268. if (key === undefined || children.length === 0) return
  269. for (let i = 0; i < children.length; i++) {
  270. const child = children[i]
  271. child.$key = String(key) + String(child.$key != null ? child.$key : i)
  272. }
  273. }
  274. function getTransitionBlocks(
  275. block: Block,
  276. onFragment?: (frag: VaporFragment) => void,
  277. ): ResolvedTransitionBlock[] {
  278. let children: ResolvedTransitionBlock[] = []
  279. if (block instanceof Element) {
  280. children.push(block)
  281. } else if (isVaporComponent(block)) {
  282. const blocks = getTransitionBlocks(block.block, onFragment)
  283. inheritKey(blocks, block.$key)
  284. children.push(...blocks)
  285. } else if (isArray(block)) {
  286. for (let i = 0; i < block.length; i++) {
  287. const b = block[i]
  288. const blocks = getTransitionBlocks(b, onFragment)
  289. if (isForBlock(b)) blocks.forEach(block => (block.$key = b.key))
  290. children.push(...blocks)
  291. }
  292. } else if (isFragment(block)) {
  293. if (isInteropEnabled && block.vnode) {
  294. // vdom component
  295. children.push(block)
  296. } else {
  297. if (onFragment) onFragment(block)
  298. const blocks = getTransitionBlocks(block.nodes, onFragment)
  299. inheritKey(blocks, block.$key)
  300. children.push(...blocks)
  301. }
  302. }
  303. return children
  304. }
  305. function isValidTransitionBlock(
  306. block: Block,
  307. ): block is ResolvedTransitionBlock {
  308. return !!(block instanceof Element || (isFragment(block) && block.vnode))
  309. }
  310. function getTransitionElement(
  311. block: ResolvedTransitionBlock,
  312. ): Element | undefined {
  313. if (block instanceof Element) return block
  314. // vdom interop
  315. if (isInteropEnabled && isFragment(block) && block.vnode) {
  316. return getTransitionElementFromVNode(block.vnode)
  317. }
  318. }
  319. function recordPosition(c: ResolvedTransitionBlock) {
  320. const el = getTransitionElement(c)
  321. if (el) newPositionMap.set(c, el.getBoundingClientRect())
  322. }
  323. function applyTranslation(
  324. c: ResolvedTransitionBlock,
  325. ): ResolvedTransitionBlock | undefined {
  326. const el = getTransitionElement(c)
  327. if (
  328. el &&
  329. baseApplyTranslation(
  330. positionMap.get(c)!,
  331. newPositionMap.get(c)!,
  332. el as ElementWithTransition,
  333. )
  334. ) {
  335. return c
  336. }
  337. }
  338. function getFirstConnectedChild(
  339. children: ResolvedTransitionBlock[],
  340. ): Element | undefined {
  341. for (let i = 0; i < children.length; i++) {
  342. const child = children[i]
  343. const el = getTransitionElement(child)
  344. if (el && el.isConnected) return el
  345. }
  346. }