TransitionGroup.ts 5.9 KB

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