TransitionGroup.ts 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. import {
  2. TransitionProps,
  3. addTransitionClass,
  4. removeTransitionClass,
  5. ElementWithTransition,
  6. getTransitionInfo,
  7. resolveTransitionProps,
  8. TransitionPropsValidators,
  9. forceReflow
  10. } from './Transition'
  11. import {
  12. Fragment,
  13. VNode,
  14. warn,
  15. resolveTransitionHooks,
  16. useTransitionState,
  17. getTransitionRawChildren,
  18. getCurrentInstance,
  19. setTransitionHooks,
  20. createVNode,
  21. onUpdated,
  22. SetupContext,
  23. toRaw,
  24. compatUtils,
  25. DeprecationTypes,
  26. ComponentOptions
  27. } from '@vue/runtime-core'
  28. import { extend } from '@vue/shared'
  29. const positionMap = new WeakMap<VNode, DOMRect>()
  30. const newPositionMap = new WeakMap<VNode, DOMRect>()
  31. export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
  32. tag?: string
  33. moveClass?: string
  34. }
  35. const TransitionGroupImpl: ComponentOptions = {
  36. name: 'TransitionGroup',
  37. props: /*#__PURE__*/ extend({}, TransitionPropsValidators, {
  38. tag: String,
  39. moveClass: String
  40. }),
  41. setup(props: TransitionGroupProps, { slots }: SetupContext) {
  42. const instance = getCurrentInstance()!
  43. const state = useTransitionState()
  44. let prevChildren: VNode[]
  45. let children: VNode[]
  46. onUpdated(() => {
  47. // children is guaranteed to exist after initial render
  48. if (!prevChildren.length) {
  49. return
  50. }
  51. const moveClass = props.moveClass || `${props.name || 'v'}-move`
  52. if (
  53. !hasCSSTransform(
  54. prevChildren[0].el as ElementWithTransition,
  55. instance.vnode.el as Node,
  56. moveClass
  57. )
  58. ) {
  59. return
  60. }
  61. // we divide the work into three loops to avoid mixing DOM reads and writes
  62. // in each iteration - which helps prevent layout thrashing.
  63. prevChildren.forEach(callPendingCbs)
  64. prevChildren.forEach(recordPosition)
  65. const movedChildren = prevChildren.filter(applyTranslation)
  66. // force reflow to put everything in position
  67. forceReflow()
  68. movedChildren.forEach(c => {
  69. const el = c.el as ElementWithTransition
  70. const style = el.style
  71. addTransitionClass(el, moveClass)
  72. style.transform = style.webkitTransform = style.transitionDuration = ''
  73. const cb = ((el as any)._moveCb = (e: TransitionEvent) => {
  74. if (e && e.target !== el) {
  75. return
  76. }
  77. if (!e || /transform$/.test(e.propertyName)) {
  78. el.removeEventListener('transitionend', cb)
  79. ;(el as any)._moveCb = null
  80. removeTransitionClass(el, moveClass)
  81. }
  82. })
  83. el.addEventListener('transitionend', cb)
  84. })
  85. })
  86. return () => {
  87. const rawProps = toRaw(props)
  88. const cssTransitionProps = resolveTransitionProps(rawProps)
  89. let tag = rawProps.tag || Fragment
  90. if (
  91. __COMPAT__ &&
  92. !rawProps.tag &&
  93. compatUtils.checkCompatEnabled(
  94. DeprecationTypes.TRANSITION_GROUP_ROOT,
  95. instance.parent
  96. )
  97. ) {
  98. tag = 'span'
  99. }
  100. prevChildren = children
  101. children = slots.default ? getTransitionRawChildren(slots.default()) : []
  102. for (let i = 0; i < children.length; i++) {
  103. const child = children[i]
  104. if (child.key != null) {
  105. setTransitionHooks(
  106. child,
  107. resolveTransitionHooks(child, cssTransitionProps, state, instance)
  108. )
  109. } else if (__DEV__) {
  110. warn(`<TransitionGroup> children must be keyed.`)
  111. }
  112. }
  113. if (prevChildren) {
  114. for (let i = 0; i < prevChildren.length; i++) {
  115. const child = prevChildren[i]
  116. setTransitionHooks(
  117. child,
  118. resolveTransitionHooks(child, cssTransitionProps, state, instance)
  119. )
  120. positionMap.set(child, (child.el as Element).getBoundingClientRect())
  121. }
  122. }
  123. return createVNode(tag, null, children)
  124. }
  125. }
  126. }
  127. if (__COMPAT__) {
  128. TransitionGroupImpl.__isBuiltIn = true
  129. }
  130. /**
  131. * TransitionGroup does not support "mode" so we need to remove it from the
  132. * props declarations, but direct delete operation is considered a side effect
  133. * and will make the entire transition feature non-tree-shakeable, so we do it
  134. * in a function and mark the function's invocation as pure.
  135. */
  136. const removeMode = (props: any) => delete props.mode
  137. /*#__PURE__*/ removeMode(TransitionGroupImpl.props)
  138. export const TransitionGroup = TransitionGroupImpl as unknown as {
  139. new (): {
  140. $props: TransitionGroupProps
  141. }
  142. }
  143. function callPendingCbs(c: VNode) {
  144. const el = c.el as any
  145. if (el._moveCb) {
  146. el._moveCb()
  147. }
  148. if (el._enterCb) {
  149. el._enterCb()
  150. }
  151. }
  152. function recordPosition(c: VNode) {
  153. newPositionMap.set(c, (c.el as Element).getBoundingClientRect())
  154. }
  155. function applyTranslation(c: VNode): VNode | undefined {
  156. const oldPos = positionMap.get(c)!
  157. const newPos = newPositionMap.get(c)!
  158. const dx = oldPos.left - newPos.left
  159. const dy = oldPos.top - newPos.top
  160. if (dx || dy) {
  161. const s = (c.el as HTMLElement).style
  162. s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)`
  163. s.transitionDuration = '0s'
  164. return c
  165. }
  166. }
  167. function hasCSSTransform(
  168. el: ElementWithTransition,
  169. root: Node,
  170. moveClass: string
  171. ): boolean {
  172. // Detect whether an element with the move class applied has
  173. // CSS transitions. Since the element may be inside an entering
  174. // transition at this very moment, we make a clone of it and remove
  175. // all other transition classes applied to ensure only the move class
  176. // is applied.
  177. const clone = el.cloneNode() as HTMLElement
  178. if (el._vtc) {
  179. el._vtc.forEach(cls => {
  180. cls.split(/\s+/).forEach(c => c && clone.classList.remove(c))
  181. })
  182. }
  183. moveClass.split(/\s+/).forEach(c => c && clone.classList.add(c))
  184. clone.style.display = 'none'
  185. const container = (
  186. root.nodeType === 1 ? root : root.parentNode
  187. ) as HTMLElement
  188. container.appendChild(clone)
  189. const { hasTransform } = getTransitionInfo(clone)
  190. container.removeChild(clone)
  191. return hasTransform
  192. }