transition.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. /* @flow */
  2. // Provides transition support for a single element/component.
  3. // supports transition mode (out-in / in-out)
  4. import { warn } from 'core/util/index'
  5. import { camelize, extend, isPrimitive } from 'shared/util'
  6. import { mergeVNodeHook, getFirstComponentChild } from 'core/vdom/helpers/index'
  7. export const transitionProps = {
  8. name: String,
  9. appear: Boolean,
  10. css: Boolean,
  11. mode: String,
  12. type: String,
  13. enterClass: String,
  14. leaveClass: String,
  15. enterToClass: String,
  16. leaveToClass: String,
  17. enterActiveClass: String,
  18. leaveActiveClass: String,
  19. appearClass: String,
  20. appearActiveClass: String,
  21. appearToClass: String,
  22. duration: [Number, String, Object]
  23. }
  24. // in case the child is also an abstract component, e.g. <keep-alive>
  25. // we want to recursively retrieve the real component to be rendered
  26. function getRealChild (vnode: ?VNode): ?VNode {
  27. const compOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
  28. if (compOptions && compOptions.Ctor.options.abstract) {
  29. return getRealChild(getFirstComponentChild(compOptions.children))
  30. } else {
  31. return vnode
  32. }
  33. }
  34. export function extractTransitionData (comp: Component): Object {
  35. const data = {}
  36. const options: ComponentOptions = comp.$options
  37. // props
  38. for (const key in options.propsData) {
  39. data[key] = comp[key]
  40. }
  41. // events.
  42. // extract listeners and pass them directly to the transition methods
  43. const listeners: ?Object = options._parentListeners
  44. for (const key in listeners) {
  45. data[camelize(key)] = listeners[key]
  46. }
  47. return data
  48. }
  49. function placeholder (h: Function, rawChild: VNode): ?VNode {
  50. if (/\d-keep-alive$/.test(rawChild.tag)) {
  51. return h('keep-alive', {
  52. props: rawChild.componentOptions.propsData
  53. })
  54. }
  55. }
  56. function hasParentTransition (vnode: VNode): ?boolean {
  57. while ((vnode = vnode.parent)) {
  58. if (vnode.data.transition) {
  59. return true
  60. }
  61. }
  62. }
  63. function isSameChild (child: VNode, oldChild: VNode): boolean {
  64. return oldChild.key === child.key && oldChild.tag === child.tag
  65. }
  66. function isAsyncPlaceholder (node: VNode): boolean {
  67. return node.isComment && node.asyncFactory
  68. }
  69. export default {
  70. name: 'transition',
  71. props: transitionProps,
  72. abstract: true,
  73. render (h: Function) {
  74. let children: ?Array<VNode> = this.$options._renderChildren
  75. if (!children) {
  76. return
  77. }
  78. // filter out text nodes (possible whitespaces)
  79. children = children.filter((c: VNode) => c.tag || isAsyncPlaceholder(c))
  80. /* istanbul ignore if */
  81. if (!children.length) {
  82. return
  83. }
  84. // warn multiple elements
  85. if (process.env.NODE_ENV !== 'production' && children.length > 1) {
  86. warn(
  87. '<transition> can only be used on a single element. Use ' +
  88. '<transition-group> for lists.',
  89. this.$parent
  90. )
  91. }
  92. const mode: string = this.mode
  93. // warn invalid mode
  94. if (process.env.NODE_ENV !== 'production' &&
  95. mode && mode !== 'in-out' && mode !== 'out-in'
  96. ) {
  97. warn(
  98. 'invalid <transition> mode: ' + mode,
  99. this.$parent
  100. )
  101. }
  102. const rawChild: VNode = children[0]
  103. // if this is a component root node and the component's
  104. // parent container node also has transition, skip.
  105. if (hasParentTransition(this.$vnode)) {
  106. return rawChild
  107. }
  108. // apply transition data to child
  109. // use getRealChild() to ignore abstract components e.g. keep-alive
  110. const child: ?VNode = getRealChild(rawChild)
  111. /* istanbul ignore if */
  112. if (!child) {
  113. return rawChild
  114. }
  115. if (this._leaving) {
  116. return placeholder(h, rawChild)
  117. }
  118. // ensure a key that is unique to the vnode type and to this transition
  119. // component instance. This key will be used to remove pending leaving nodes
  120. // during entering.
  121. const id: string = `__transition-${this._uid}-`
  122. child.key = child.key == null
  123. ? id + child.tag
  124. : isPrimitive(child.key)
  125. ? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
  126. : child.key
  127. const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
  128. const oldRawChild: VNode = this._vnode
  129. const oldChild: VNode = getRealChild(oldRawChild)
  130. // mark v-show
  131. // so that the transition module can hand over the control to the directive
  132. if (child.data.directives && child.data.directives.some(d => d.name === 'show')) {
  133. child.data.show = true
  134. }
  135. if (
  136. oldChild &&
  137. oldChild.data &&
  138. !isSameChild(child, oldChild) &&
  139. !isAsyncPlaceholder(oldChild)
  140. ) {
  141. // replace old child transition data with fresh one
  142. // important for dynamic transitions!
  143. const oldData: Object = oldChild && (oldChild.data.transition = extend({}, data))
  144. // handle transition mode
  145. if (mode === 'out-in') {
  146. // return placeholder node and queue update when leave finishes
  147. this._leaving = true
  148. mergeVNodeHook(oldData, 'afterLeave', () => {
  149. this._leaving = false
  150. this.$forceUpdate()
  151. })
  152. return placeholder(h, rawChild)
  153. } else if (mode === 'in-out') {
  154. if (isAsyncPlaceholder(child)) {
  155. return oldRawChild
  156. }
  157. let delayedLeave
  158. const performLeave = () => { delayedLeave() }
  159. mergeVNodeHook(data, 'afterEnter', performLeave)
  160. mergeVNodeHook(data, 'enterCancelled', performLeave)
  161. mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })
  162. }
  163. }
  164. return rawChild
  165. }
  166. }