TransitionGroup.ts 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205
  1. import {
  2. TransitionProps,
  3. addTransitionClass,
  4. removeTransitionClass,
  5. ElementWithTransition,
  6. getTransitionInfo,
  7. resolveTransitionProps,
  8. TransitionPropsValidators
  9. } from './Transition'
  10. import {
  11. Fragment,
  12. VNode,
  13. warn,
  14. resolveTransitionHooks,
  15. useTransitionState,
  16. getTransitionRawChildren,
  17. getCurrentInstance,
  18. setTransitionHooks,
  19. createVNode,
  20. onUpdated,
  21. SetupContext
  22. } from '@vue/runtime-core'
  23. import { toRaw } from '@vue/reactivity'
  24. import { extend } from '@vue/shared'
  25. interface Position {
  26. top: number
  27. left: number
  28. }
  29. const positionMap = new WeakMap<VNode, Position>()
  30. const newPositionMap = new WeakMap<VNode, Position>()
  31. export type TransitionGroupProps = Omit<TransitionProps, 'mode'> & {
  32. tag?: string
  33. moveClass?: string
  34. }
  35. const TransitionGroupImpl = {
  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. const tag = rawProps.tag || Fragment
  90. prevChildren = children
  91. children = slots.default ? getTransitionRawChildren(slots.default()) : []
  92. for (let i = 0; i < children.length; i++) {
  93. const child = children[i]
  94. if (child.key != null) {
  95. setTransitionHooks(
  96. child,
  97. resolveTransitionHooks(child, cssTransitionProps, state, instance)
  98. )
  99. } else if (__DEV__) {
  100. warn(`<TransitionGroup> children must be keyed.`)
  101. }
  102. }
  103. if (prevChildren) {
  104. for (let i = 0; i < prevChildren.length; i++) {
  105. const child = prevChildren[i]
  106. setTransitionHooks(
  107. child,
  108. resolveTransitionHooks(child, cssTransitionProps, state, instance)
  109. )
  110. positionMap.set(child, (child.el as Element).getBoundingClientRect())
  111. }
  112. }
  113. return createVNode(tag, null, children)
  114. }
  115. }
  116. }
  117. /**
  118. * TransitionGroup does not support "mode" so we need to remove it from the
  119. * props declarations, but direct delete operation is considered a side effect
  120. * and will make the entire transition feature non-tree-shakeable, so we do it
  121. * in a function and mark the function's invocation as pure.
  122. */
  123. const removeMode = (props: any) => delete props.mode
  124. /*#__PURE__*/ removeMode(TransitionGroupImpl.props)
  125. export const TransitionGroup = (TransitionGroupImpl as unknown) as {
  126. new (): {
  127. $props: TransitionGroupProps
  128. }
  129. }
  130. function callPendingCbs(c: VNode) {
  131. const el = c.el as any
  132. if (el._moveCb) {
  133. el._moveCb()
  134. }
  135. if (el._enterCb) {
  136. el._enterCb()
  137. }
  138. }
  139. function recordPosition(c: VNode) {
  140. newPositionMap.set(c, (c.el as Element).getBoundingClientRect())
  141. }
  142. function applyTranslation(c: VNode): VNode | undefined {
  143. const oldPos = positionMap.get(c)!
  144. const newPos = newPositionMap.get(c)!
  145. const dx = oldPos.left - newPos.left
  146. const dy = oldPos.top - newPos.top
  147. if (dx || dy) {
  148. const s = (c.el as HTMLElement).style
  149. s.transform = s.webkitTransform = `translate(${dx}px,${dy}px)`
  150. s.transitionDuration = '0s'
  151. return c
  152. }
  153. }
  154. // this is put in a dedicated function to avoid the line from being treeshaken
  155. function forceReflow() {
  156. return document.body.offsetHeight
  157. }
  158. function hasCSSTransform(
  159. el: ElementWithTransition,
  160. root: Node,
  161. moveClass: string
  162. ): boolean {
  163. // Detect whether an element with the move class applied has
  164. // CSS transitions. Since the element may be inside an entering
  165. // transition at this very moment, we make a clone of it and remove
  166. // all other transition classes applied to ensure only the move class
  167. // is applied.
  168. const clone = el.cloneNode() as HTMLElement
  169. if (el._vtc) {
  170. el._vtc.forEach(cls => {
  171. cls.split(/\s+/).forEach(c => c && clone.classList.remove(c))
  172. })
  173. }
  174. moveClass.split(/\s+/).forEach(c => c && clone.classList.add(c))
  175. clone.style.display = 'none'
  176. const container = (root.nodeType === 1
  177. ? root
  178. : root.parentNode) as HTMLElement
  179. container.appendChild(clone)
  180. const { hasTransform } = getTransitionInfo(clone)
  181. container.removeChild(clone)
  182. return hasTransform
  183. }