transition.js 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304
  1. /* @flow */
  2. import { once, isObject } from 'shared/util'
  3. import { inBrowser, isIE9, warn } from 'core/util/index'
  4. import { mergeVNodeHook } from 'core/vdom/helpers/index'
  5. import { activeInstance } from 'core/instance/lifecycle'
  6. import {
  7. nextFrame,
  8. resolveTransition,
  9. whenTransitionEnds,
  10. addTransitionClass,
  11. removeTransitionClass
  12. } from '../transition-util'
  13. export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) {
  14. const el: any = vnode.elm
  15. // call leave callback now
  16. if (el._leaveCb) {
  17. el._leaveCb.cancelled = true
  18. el._leaveCb()
  19. }
  20. const data = resolveTransition(vnode.data.transition)
  21. if (!data) {
  22. return
  23. }
  24. /* istanbul ignore if */
  25. if (el._enterCb || el.nodeType !== 1) {
  26. return
  27. }
  28. const {
  29. css,
  30. type,
  31. enterClass,
  32. enterToClass,
  33. enterActiveClass,
  34. appearClass,
  35. appearToClass,
  36. appearActiveClass,
  37. beforeEnter,
  38. enter,
  39. afterEnter,
  40. enterCancelled,
  41. beforeAppear,
  42. appear,
  43. afterAppear,
  44. appearCancelled,
  45. duration
  46. } = data
  47. // activeInstance will always be the <transition> component managing this
  48. // transition. One edge case to check is when the <transition> is placed
  49. // as the root node of a child component. In that case we need to check
  50. // <transition>'s parent for appear check.
  51. let context = activeInstance
  52. let transitionNode = activeInstance.$vnode
  53. while (transitionNode && transitionNode.parent) {
  54. transitionNode = transitionNode.parent
  55. context = transitionNode.context
  56. }
  57. const isAppear = !context._isMounted || !vnode.isRootInsert
  58. if (isAppear && !appear && appear !== '') {
  59. return
  60. }
  61. const startClass = isAppear && appearClass ? appearClass : enterClass
  62. const activeClass = isAppear && appearActiveClass ? appearActiveClass : enterActiveClass
  63. const toClass = isAppear && appearToClass ? appearToClass : enterToClass
  64. const beforeEnterHook = isAppear ? (beforeAppear || beforeEnter) : beforeEnter
  65. const enterHook = isAppear ? (typeof appear === 'function' ? appear : enter) : enter
  66. const afterEnterHook = isAppear ? (afterAppear || afterEnter) : afterEnter
  67. const enterCancelledHook = isAppear ? (appearCancelled || enterCancelled) : enterCancelled
  68. const explicitEnterDuration = isObject(duration) ? duration.enter : duration
  69. if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) {
  70. checkDuration(explicitEnterDuration, 'enter', vnode)
  71. }
  72. const expectsCSS = css !== false && !isIE9
  73. const userWantsControl = getHookAgumentsLength(enterHook)
  74. const cb = el._enterCb = once(() => {
  75. if (expectsCSS) {
  76. removeTransitionClass(el, toClass)
  77. removeTransitionClass(el, activeClass)
  78. }
  79. if (cb.cancelled) {
  80. if (expectsCSS) {
  81. removeTransitionClass(el, startClass)
  82. }
  83. enterCancelledHook && enterCancelledHook(el)
  84. } else {
  85. afterEnterHook && afterEnterHook(el)
  86. }
  87. el._enterCb = null
  88. })
  89. if (!vnode.data.show) {
  90. // remove pending leave element on enter by injecting an insert hook
  91. mergeVNodeHook(vnode.data.hook || (vnode.data.hook = {}), 'insert', () => {
  92. const parent = el.parentNode
  93. const pendingNode = parent && parent._pending && parent._pending[vnode.key]
  94. if (pendingNode &&
  95. pendingNode.tag === vnode.tag &&
  96. pendingNode.elm._leaveCb) {
  97. pendingNode.elm._leaveCb()
  98. }
  99. enterHook && enterHook(el, cb)
  100. })
  101. }
  102. // start enter transition
  103. beforeEnterHook && beforeEnterHook(el)
  104. if (expectsCSS) {
  105. addTransitionClass(el, startClass)
  106. addTransitionClass(el, activeClass)
  107. nextFrame(() => {
  108. addTransitionClass(el, toClass)
  109. removeTransitionClass(el, startClass)
  110. if (!cb.cancelled && !userWantsControl) {
  111. if (isValidDuration(explicitEnterDuration)) {
  112. setTimeout(cb, explicitEnterDuration)
  113. } else {
  114. whenTransitionEnds(el, type, cb)
  115. }
  116. }
  117. })
  118. }
  119. if (vnode.data.show) {
  120. toggleDisplay && toggleDisplay()
  121. enterHook && enterHook(el, cb)
  122. }
  123. if (!expectsCSS && !userWantsControl) {
  124. cb()
  125. }
  126. }
  127. export function leave (vnode: VNodeWithData, rm: Function) {
  128. const el: any = vnode.elm
  129. // call enter callback now
  130. if (el._enterCb) {
  131. el._enterCb.cancelled = true
  132. el._enterCb()
  133. }
  134. const data = resolveTransition(vnode.data.transition)
  135. if (!data) {
  136. return rm()
  137. }
  138. /* istanbul ignore if */
  139. if (el._leaveCb || el.nodeType !== 1) {
  140. return
  141. }
  142. const {
  143. css,
  144. type,
  145. leaveClass,
  146. leaveToClass,
  147. leaveActiveClass,
  148. beforeLeave,
  149. leave,
  150. afterLeave,
  151. leaveCancelled,
  152. delayLeave,
  153. duration
  154. } = data
  155. const expectsCSS = css !== false && !isIE9
  156. const userWantsControl = getHookAgumentsLength(leave)
  157. const explicitLeaveDuration = isObject(duration) ? duration.leave : duration
  158. if (process.env.NODE_ENV !== 'production' && explicitLeaveDuration != null) {
  159. checkDuration(explicitLeaveDuration, 'leave', vnode)
  160. }
  161. const cb = el._leaveCb = once(() => {
  162. if (el.parentNode && el.parentNode._pending) {
  163. el.parentNode._pending[vnode.key] = null
  164. }
  165. if (expectsCSS) {
  166. removeTransitionClass(el, leaveToClass)
  167. removeTransitionClass(el, leaveActiveClass)
  168. }
  169. if (cb.cancelled) {
  170. if (expectsCSS) {
  171. removeTransitionClass(el, leaveClass)
  172. }
  173. leaveCancelled && leaveCancelled(el)
  174. } else {
  175. rm()
  176. afterLeave && afterLeave(el)
  177. }
  178. el._leaveCb = null
  179. })
  180. if (delayLeave) {
  181. delayLeave(performLeave)
  182. } else {
  183. performLeave()
  184. }
  185. function performLeave () {
  186. // the delayed leave may have already been cancelled
  187. if (cb.cancelled) {
  188. return
  189. }
  190. // record leaving element
  191. if (!vnode.data.show) {
  192. (el.parentNode._pending || (el.parentNode._pending = {}))[vnode.key] = vnode
  193. }
  194. beforeLeave && beforeLeave(el)
  195. if (expectsCSS) {
  196. addTransitionClass(el, leaveClass)
  197. addTransitionClass(el, leaveActiveClass)
  198. nextFrame(() => {
  199. addTransitionClass(el, leaveToClass)
  200. removeTransitionClass(el, leaveClass)
  201. if (!cb.cancelled && !userWantsControl) {
  202. if (isValidDuration(explicitLeaveDuration)) {
  203. setTimeout(cb, explicitLeaveDuration)
  204. } else {
  205. whenTransitionEnds(el, type, cb)
  206. }
  207. }
  208. })
  209. }
  210. leave && leave(el, cb)
  211. if (!expectsCSS && !userWantsControl) {
  212. cb()
  213. }
  214. }
  215. }
  216. // only used in dev mode
  217. function checkDuration (val, name, vnode) {
  218. if (typeof val !== 'number') {
  219. warn(
  220. `<transition> explicit ${name} duration is not a valid number - ` +
  221. `got ${JSON.stringify(val)}.`,
  222. vnode.context
  223. )
  224. } else if (isNaN(val)) {
  225. warn(
  226. `<transition> explicit ${name} duration is NaN - ` +
  227. 'the duration expression might be incorrect.',
  228. vnode.context
  229. )
  230. }
  231. }
  232. function isValidDuration (val) {
  233. return typeof val === 'number' && !isNaN(val)
  234. }
  235. /**
  236. * Normalize a transition hook's argument length. The hook may be:
  237. * - a merged hook (invoker) with the original in .fns
  238. * - a wrapped component method (check ._length)
  239. * - a plain function (.length)
  240. */
  241. function getHookAgumentsLength (fn: Function): boolean {
  242. if (!fn) return false
  243. const invokerFns = fn.fns
  244. if (invokerFns) {
  245. // invoker
  246. return getHookAgumentsLength(
  247. Array.isArray(invokerFns)
  248. ? invokerFns[0]
  249. : invokerFns
  250. )
  251. } else {
  252. return (fn._length || fn.length) > 1
  253. }
  254. }
  255. function _enter (_: any, vnode: VNodeWithData) {
  256. if (!vnode.data.show) {
  257. enter(vnode)
  258. }
  259. }
  260. export default inBrowser ? {
  261. create: _enter,
  262. activate: _enter,
  263. remove (vnode: VNode, rm: Function) {
  264. /* istanbul ignore else */
  265. if (!vnode.data.show) {
  266. leave(vnode, rm)
  267. } else {
  268. rm()
  269. }
  270. }
  271. } : {}