f93ce8de96b16c8dfc6d972cab9c822c0f85c627a472c7b0a251a4f945b2893e6916b9c21c4bdfbd29a6c26cb6b8ae016ed8287e8cc8a20d8ec33b15db0b02 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /* @flow */
  2. // Provides transition support for list items.
  3. // supports move transitions using the FLIP technique.
  4. // Because the vdom's children update algorithm is "unstable" - i.e.
  5. // it doesn't guarantee the relative positioning of removed elements,
  6. // we force transition-group to update its children into two passes:
  7. // in the first pass, we remove all nodes that need to be removed,
  8. // triggering their leaving transition; in the second pass, we insert/move
  9. // into the final desired state. This way in the second pass removed
  10. // nodes will remain where they should be.
  11. import { warn, extend } from 'core/util/index'
  12. import { addClass, removeClass } from '../class-util'
  13. import { transitionProps, extractTransitionData } from './transition'
  14. import { setActiveInstance } from 'core/instance/lifecycle'
  15. import {
  16. hasTransition,
  17. getTransitionInfo,
  18. transitionEndEvent,
  19. addTransitionClass,
  20. removeTransitionClass
  21. } from '../transition-util'
  22. const props = extend({
  23. tag: String,
  24. moveClass: String
  25. }, transitionProps)
  26. delete props.mode
  27. export default {
  28. props,
  29. beforeMount () {
  30. const update = this._update
  31. this._update = (vnode, hydrating) => {
  32. const restoreActiveInstance = setActiveInstance(this)
  33. // force removing pass
  34. this.__patch__(
  35. this._vnode,
  36. this.kept,
  37. false, // hydrating
  38. true // removeOnly (!important, avoids unnecessary moves)
  39. )
  40. this._vnode = this.kept
  41. restoreActiveInstance()
  42. update.call(this, vnode, hydrating)
  43. }
  44. },
  45. render (h: Function) {
  46. const tag: string = this.tag || this.$vnode.data.tag || 'span'
  47. const map: Object = Object.create(null)
  48. const prevChildren: Array<VNode> = this.prevChildren = this.children
  49. const rawChildren: Array<VNode> = this.$slots.default || []
  50. const children: Array<VNode> = this.children = []
  51. const transitionData: Object = extractTransitionData(this)
  52. for (let i = 0; i < rawChildren.length; i++) {
  53. const c: VNode = rawChildren[i]
  54. if (c.tag) {
  55. if (c.key != null && String(c.key).indexOf('__vlist') !== 0) {
  56. children.push(c)
  57. map[c.key] = c
  58. ;(c.data || (c.data = {})).transition = transitionData
  59. } else if (process.env.NODE_ENV !== 'production') {
  60. const opts: ?VNodeComponentOptions = c.componentOptions
  61. const name: string = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag
  62. warn(`<transition-group> children must be keyed: <${name}>`)
  63. }
  64. }
  65. }
  66. if (prevChildren) {
  67. const kept: Array<VNode> = []
  68. const removed: Array<VNode> = []
  69. for (let i = 0; i < prevChildren.length; i++) {
  70. const c: VNode = prevChildren[i]
  71. c.data.transition = transitionData
  72. c.data.pos = c.elm.getBoundingClientRect()
  73. if (map[c.key]) {
  74. kept.push(c)
  75. } else {
  76. removed.push(c)
  77. }
  78. }
  79. this.kept = h(tag, null, kept)
  80. this.removed = removed
  81. }
  82. return h(tag, null, children)
  83. },
  84. updated () {
  85. const children: Array<VNode> = this.prevChildren
  86. const moveClass: string = this.moveClass || ((this.name || 'v') + '-move')
  87. if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
  88. return
  89. }
  90. // we divide the work into three loops to avoid mixing DOM reads and writes
  91. // in each iteration - which helps prevent layout thrashing.
  92. children.forEach(callPendingCbs)
  93. children.forEach(recordPosition)
  94. children.forEach(applyTranslation)
  95. // force reflow to put everything in position
  96. // assign to this to avoid being removed in tree-shaking
  97. // $flow-disable-line
  98. this._reflow = document.body.offsetHeight
  99. children.forEach((c: VNode) => {
  100. if (c.data.moved) {
  101. const el: any = c.elm
  102. const s: any = el.style
  103. addTransitionClass(el, moveClass)
  104. s.transform = s.WebkitTransform = s.transitionDuration = ''
  105. el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) {
  106. if (e && e.target !== el) {
  107. return
  108. }
  109. if (!e || /transform$/.test(e.propertyName)) {
  110. el.removeEventListener(transitionEndEvent, cb)
  111. el._moveCb = null
  112. removeTransitionClass(el, moveClass)
  113. }
  114. })
  115. }
  116. })
  117. },
  118. methods: {
  119. hasMove (el: any, moveClass: string): boolean {
  120. /* istanbul ignore if */
  121. if (!hasTransition) {
  122. return false
  123. }
  124. /* istanbul ignore if */
  125. if (this._hasMove) {
  126. return this._hasMove
  127. }
  128. // Detect whether an element with the move class applied has
  129. // CSS transitions. Since the element may be inside an entering
  130. // transition at this very moment, we make a clone of it and remove
  131. // all other transition classes applied to ensure only the move class
  132. // is applied.
  133. const clone: HTMLElement = el.cloneNode()
  134. if (el._transitionClasses) {
  135. el._transitionClasses.forEach((cls: string) => { removeClass(clone, cls) })
  136. }
  137. addClass(clone, moveClass)
  138. clone.style.display = 'none'
  139. this.$el.appendChild(clone)
  140. const info: Object = getTransitionInfo(clone)
  141. this.$el.removeChild(clone)
  142. return (this._hasMove = info.hasTransform)
  143. }
  144. }
  145. }
  146. function callPendingCbs (c: VNode) {
  147. /* istanbul ignore if */
  148. if (c.elm._moveCb) {
  149. c.elm._moveCb()
  150. }
  151. /* istanbul ignore if */
  152. if (c.elm._enterCb) {
  153. c.elm._enterCb()
  154. }
  155. }
  156. function recordPosition (c: VNode) {
  157. c.data.newPos = c.elm.getBoundingClientRect()
  158. }
  159. function applyTranslation (c: VNode) {
  160. const oldPos = c.data.pos
  161. const newPos = c.data.newPos
  162. const dx = oldPos.left - newPos.left
  163. const dy = oldPos.top - newPos.top
  164. if (dx || dy) {
  165. c.data.moved = true
  166. const s = c.elm.style
  167. s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
  168. s.transitionDuration = '0s'
  169. }
  170. }