Transition.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379
  1. import {
  2. type BaseTransitionProps,
  3. type GenericComponentInstance,
  4. type TransitionElement,
  5. type TransitionHooks,
  6. type TransitionHooksContext,
  7. type TransitionProps,
  8. TransitionPropsValidators,
  9. type TransitionState,
  10. baseResolveTransitionHooks,
  11. checkTransitionMode,
  12. currentInstance,
  13. getComponentName,
  14. isAsyncWrapper,
  15. isTemplateNode,
  16. leaveCbKey,
  17. queuePostFlushCb,
  18. resolveTransitionProps,
  19. useTransitionState,
  20. warn,
  21. } from '@vue/runtime-dom'
  22. import {
  23. type Block,
  24. type TransitionBlock,
  25. type VaporTransitionHooks,
  26. registerTransitionHooks,
  27. } from '../block'
  28. import {
  29. type FunctionalVaporComponent,
  30. type VaporComponentInstance,
  31. isVaporComponent,
  32. } from '../component'
  33. import { isArray } from '@vue/shared'
  34. import { renderEffect } from '../renderEffect'
  35. import {
  36. type DynamicFragment,
  37. type VaporFragment,
  38. isFragment,
  39. } from '../fragment'
  40. import {
  41. currentHydrationNode,
  42. isHydrating,
  43. setCurrentHydrationNode,
  44. } from '../dom/hydration'
  45. const displayName = 'VaporTransition'
  46. let registered = false
  47. export const ensureTransitionHooksRegistered = (): void => {
  48. if (!registered) {
  49. registered = true
  50. registerTransitionHooks(
  51. applyTransitionHooksImpl,
  52. applyTransitionLeaveHooksImpl,
  53. )
  54. }
  55. }
  56. const hydrateTransitionImpl = () => {
  57. if (!currentHydrationNode || !isTemplateNode(currentHydrationNode)) return
  58. // replace <template> node with inner child
  59. const {
  60. content: { firstChild },
  61. parentNode,
  62. } = currentHydrationNode
  63. if (firstChild) {
  64. parentNode!.replaceChild(firstChild, currentHydrationNode)
  65. setCurrentHydrationNode(firstChild)
  66. if (firstChild instanceof HTMLElement || firstChild instanceof SVGElement) {
  67. const originalDisplay = firstChild.style.display
  68. firstChild.style.display = 'none'
  69. return (hooks: TransitionHooks) => {
  70. hooks.beforeEnter(firstChild)
  71. firstChild.style.display = originalDisplay
  72. queuePostFlushCb(() => hooks.enter(firstChild))
  73. }
  74. }
  75. }
  76. }
  77. const decorate = (t: typeof VaporTransition) => {
  78. t.displayName = displayName
  79. t.props = TransitionPropsValidators
  80. t.__vapor = true
  81. return t
  82. }
  83. export const VaporTransition: FunctionalVaporComponent<TransitionProps> =
  84. /*@__PURE__*/ decorate((props, { slots, expose }) => {
  85. // @ts-expect-error
  86. expose()
  87. // Register transition hooks on first use
  88. ensureTransitionHooksRegistered()
  89. const performAppear = isHydrating ? hydrateTransitionImpl() : undefined
  90. const children = (slots.default && slots.default()) as any as Block
  91. if (!children) return []
  92. const instance = currentInstance! as VaporComponentInstance
  93. const { mode } = props
  94. checkTransitionMode(mode)
  95. let resolvedProps: BaseTransitionProps<Element>
  96. renderEffect(() => (resolvedProps = resolveTransitionProps(props)))
  97. const hooks = applyTransitionHooksImpl(children, {
  98. state: useTransitionState(),
  99. // use proxy to keep props reference stable
  100. props: new Proxy({} as BaseTransitionProps<Element>, {
  101. get(_, key) {
  102. return resolvedProps[key as keyof BaseTransitionProps<Element>]
  103. },
  104. }),
  105. instance: instance,
  106. } as VaporTransitionHooks)
  107. if (resolvedProps!.appear && performAppear) {
  108. performAppear(hooks)
  109. }
  110. return children
  111. })
  112. const getTransitionHooksContext = (
  113. key: string,
  114. props: TransitionProps,
  115. state: TransitionState,
  116. instance: GenericComponentInstance,
  117. postClone: ((hooks: TransitionHooks) => void) | undefined,
  118. ) => {
  119. const { leavingNodes } = state
  120. const context: TransitionHooksContext = {
  121. isLeaving: () => leavingNodes.has(key),
  122. setLeavingNodeCache: el => {
  123. leavingNodes.set(key, el)
  124. },
  125. unsetLeavingNodeCache: el => {
  126. const leavingNode = leavingNodes.get(key)
  127. if (leavingNode === el) {
  128. leavingNodes.delete(key)
  129. }
  130. },
  131. earlyRemove: () => {
  132. const leavingNode = leavingNodes.get(key)
  133. if (leavingNode && (leavingNode as TransitionElement)[leaveCbKey]) {
  134. // force early removal (not cancelled)
  135. ;(leavingNode as TransitionElement)[leaveCbKey]!()
  136. }
  137. },
  138. cloneHooks: block => {
  139. const hooks = resolveTransitionHooks(
  140. block,
  141. props,
  142. state,
  143. instance,
  144. postClone,
  145. )
  146. if (postClone) postClone(hooks)
  147. return hooks
  148. },
  149. }
  150. return context
  151. }
  152. export function resolveTransitionHooks(
  153. block: TransitionBlock,
  154. props: TransitionProps,
  155. state: TransitionState,
  156. instance: GenericComponentInstance,
  157. postClone?: (hooks: TransitionHooks) => void,
  158. ): VaporTransitionHooks {
  159. const context = getTransitionHooksContext(
  160. String(block.$key),
  161. props,
  162. state,
  163. instance,
  164. postClone,
  165. )
  166. const hooks = baseResolveTransitionHooks(
  167. context,
  168. props,
  169. state,
  170. instance,
  171. ) as VaporTransitionHooks
  172. hooks.state = state
  173. hooks.props = props
  174. hooks.instance = instance as VaporComponentInstance
  175. return hooks
  176. }
  177. function applyTransitionHooksImpl(
  178. block: Block,
  179. hooks: VaporTransitionHooks,
  180. ): VaporTransitionHooks {
  181. // filter out comment nodes
  182. if (isArray(block)) {
  183. block = block.filter(b => !(b instanceof Comment))
  184. if (block.length === 1) {
  185. block = block[0]
  186. } else if (block.length === 0) {
  187. return hooks
  188. }
  189. }
  190. const fragments: VaporFragment[] = []
  191. const child = findTransitionBlock(block, frag => fragments.push(frag))
  192. if (!child) {
  193. // set transition hooks on fragments for later use
  194. fragments.forEach(f => (f.$transition = hooks))
  195. // warn if no child and no fragments
  196. if (__DEV__ && fragments.length === 0) {
  197. warn('Transition component has no valid child element')
  198. }
  199. return hooks
  200. }
  201. const { props, instance, state, delayedLeave } = hooks
  202. let resolvedHooks = resolveTransitionHooks(
  203. child,
  204. props,
  205. state,
  206. instance,
  207. hooks => (resolvedHooks = hooks as VaporTransitionHooks),
  208. )
  209. resolvedHooks.delayedLeave = delayedLeave
  210. child.$transition = resolvedHooks
  211. fragments.forEach(f => (f.$transition = resolvedHooks))
  212. return resolvedHooks
  213. }
  214. function applyTransitionLeaveHooksImpl(
  215. block: Block,
  216. enterHooks: VaporTransitionHooks,
  217. afterLeaveCb: () => void,
  218. ): void {
  219. const leavingBlock = findTransitionBlock(block)
  220. if (!leavingBlock) return undefined
  221. const { props, state, instance } = enterHooks
  222. const leavingHooks = resolveTransitionHooks(
  223. leavingBlock,
  224. props,
  225. state,
  226. instance,
  227. )
  228. leavingBlock.$transition = leavingHooks
  229. const { mode } = props
  230. if (mode === 'out-in') {
  231. state.isLeaving = true
  232. leavingHooks.afterLeave = () => {
  233. state.isLeaving = false
  234. afterLeaveCb()
  235. leavingBlock.$transition = undefined
  236. delete leavingHooks.afterLeave
  237. }
  238. } else if (mode === 'in-out') {
  239. leavingHooks.delayLeave = (
  240. block: TransitionElement,
  241. earlyRemove,
  242. delayedLeave,
  243. ) => {
  244. const leavingKey = String(leavingBlock.$key)
  245. state.leavingNodes.set(leavingKey, leavingBlock)
  246. // Bind cleanup to this specific handoff so an older leave callback
  247. // cannot clear a newer delayedLeave during rapid toggles.
  248. const delayedLeaveCb = () => {
  249. delayedLeave()
  250. leavingBlock.$transition = undefined
  251. if (enterHooks.delayedLeave === delayedLeaveCb) {
  252. delete enterHooks.delayedLeave
  253. }
  254. }
  255. // early removal callback
  256. block[leaveCbKey] = () => {
  257. earlyRemove()
  258. block[leaveCbKey] = undefined
  259. leavingBlock.$transition = undefined
  260. // Same-key in-out switches early-remove the previous leaving block.
  261. // Clear the cache entry so the next enter isn't skipped as "still leaving".
  262. if (state.leavingNodes.get(leavingKey) === leavingBlock) {
  263. state.leavingNodes.delete(leavingKey)
  264. }
  265. if (enterHooks.delayedLeave === delayedLeaveCb) {
  266. delete enterHooks.delayedLeave
  267. }
  268. }
  269. enterHooks.delayedLeave = delayedLeaveCb
  270. }
  271. }
  272. }
  273. export function findTransitionBlock(
  274. block: Block,
  275. onFragment?: (frag: VaporFragment) => void,
  276. ): TransitionBlock | undefined {
  277. let child: TransitionBlock | undefined
  278. if (block instanceof Node) {
  279. // transition can only be applied on Element child
  280. if (block instanceof Element) child = block
  281. } else if (isVaporComponent(block)) {
  282. if (isAsyncWrapper(block)) {
  283. // for unresolved async wrapper, set transition hooks on inner fragment
  284. if (!block.type.__asyncResolved) {
  285. onFragment && onFragment(block.block! as DynamicFragment)
  286. } else {
  287. child = findTransitionBlock(
  288. (block.block! as DynamicFragment).nodes,
  289. onFragment,
  290. )
  291. }
  292. } else {
  293. // stop searching if encountering nested Transition component
  294. if (getComponentName(block.type) === displayName) return undefined
  295. child = findTransitionBlock(block.block, onFragment)
  296. // use component id as key
  297. if (child && child.$key === undefined) child.$key = block.uid
  298. }
  299. } else if (isArray(block)) {
  300. let hasFound = false
  301. for (const c of block) {
  302. if (c instanceof Comment) continue
  303. const item = findTransitionBlock(c, onFragment)
  304. if (__DEV__ && hasFound) {
  305. // warn more than one non-comment child
  306. warn(
  307. '<transition> can only be used on a single element or component. ' +
  308. 'Use <transition-group> for lists.',
  309. )
  310. break
  311. }
  312. child = item
  313. hasFound = true
  314. if (!__DEV__) break
  315. }
  316. } else if (isFragment(block)) {
  317. if (block.insert) {
  318. child = block
  319. } else {
  320. // collect fragments for setting transition hooks
  321. if (onFragment) onFragment(block)
  322. child = findTransitionBlock(block.nodes, onFragment)
  323. }
  324. }
  325. return child
  326. }
  327. export function setTransitionHooksOnFragment(
  328. block: Block,
  329. hooks: VaporTransitionHooks,
  330. ): void {
  331. if (isFragment(block)) {
  332. block.$transition = hooks
  333. if (block.nodes && isFragment(block.nodes)) {
  334. setTransitionHooksOnFragment(block.nodes, hooks)
  335. }
  336. } else if (isArray(block)) {
  337. for (let i = 0; i < block.length; i++) {
  338. setTransitionHooksOnFragment(block[i], hooks)
  339. }
  340. }
  341. }
  342. export function setTransitionHooks(
  343. block: TransitionBlock,
  344. hooks: VaporTransitionHooks,
  345. ): void {
  346. if (isVaporComponent(block)) {
  347. block = findTransitionBlock(block.block) as TransitionBlock
  348. if (!block) return
  349. }
  350. block.$transition = hooks
  351. }