transition.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269
  1. var _ = require('../util')
  2. var queue = require('./queue')
  3. var addClass = _.addClass
  4. var removeClass = _.removeClass
  5. var transitionEndEvent = _.transitionEndEvent
  6. var animationEndEvent = _.animationEndEvent
  7. var transDurationProp = _.transitionProp + 'Duration'
  8. var animDurationProp = _.animationProp + 'Duration'
  9. var doc = typeof document === 'undefined' ? null : document
  10. var TYPE_TRANSITION = 1
  11. var TYPE_ANIMATION = 2
  12. /**
  13. * A Transition object that encapsulates the state and logic
  14. * of the transition.
  15. *
  16. * @param {Element} el
  17. * @param {String} id
  18. * @param {Object} hooks
  19. * @param {Vue} vm
  20. */
  21. function Transition (el, id, hooks, vm) {
  22. this.el = el
  23. this.enterClass = id + '-enter'
  24. this.leaveClass = id + '-leave'
  25. this.hooks = hooks
  26. this.vm = vm
  27. // async state
  28. this.pendingCssEvent =
  29. this.pendingCssCb =
  30. this.jsCancel =
  31. this.pendingJsCb =
  32. this.op =
  33. this.cb = null
  34. this.typeCache = {}
  35. // bind
  36. var self = this
  37. ;['enterNextTick', 'enterDone', 'leaveNextTick', 'leaveDone']
  38. .forEach(function (m) {
  39. self[m] = _.bind(self[m], self)
  40. })
  41. }
  42. var p = Transition.prototype
  43. /**
  44. * Start an entering transition.
  45. *
  46. * @param {Function} op - insert/show the element
  47. * @param {Function} [cb]
  48. */
  49. p.enter = function (op, cb) {
  50. this.cancelPending()
  51. this.callHook('beforeEnter')
  52. this.cb = cb
  53. addClass(this.el, this.enterClass)
  54. op()
  55. this.callHookWithCb('enter')
  56. queue.push(this.enterNextTick)
  57. }
  58. /**
  59. * The "nextTick" phase of an entering transition, which is
  60. * to be pushed into a queue and executed after a reflow so
  61. * that removing the class can trigger a CSS transition.
  62. */
  63. p.enterNextTick = function () {
  64. var type = this.getCssTransitionType(this.enterClass)
  65. var enterDone = this.enterDone
  66. if (type === TYPE_TRANSITION) {
  67. // trigger transition by removing enter class now
  68. removeClass(this.el, this.enterClass)
  69. this.setupCssCb(transitionEndEvent, enterDone)
  70. } else if (type === TYPE_ANIMATION) {
  71. this.setupCssCb(animationEndEvent, enterDone)
  72. } else if (!this.pendingJsCb) {
  73. enterDone()
  74. }
  75. }
  76. /**
  77. * The "cleanup" phase of an entering transition.
  78. */
  79. p.enterDone = function () {
  80. this.jsCancel = this.pendingJsCb = null
  81. removeClass(this.el, this.enterClass)
  82. this.callHook('afterEnter')
  83. if (this.cb) this.cb()
  84. }
  85. /**
  86. * Start a leaving transition.
  87. *
  88. * @param {Function} op - remove/hide the element
  89. * @param {Function} [cb]
  90. */
  91. p.leave = function (op, cb) {
  92. this.cancelPending()
  93. this.callHook('beforeLeave')
  94. this.op = op
  95. this.cb = cb
  96. addClass(this.el, this.leaveClass)
  97. this.callHookWithCb('leave')
  98. // only need to do leaveNextTick if there's no explicit
  99. // js callback
  100. if (!this.pendingJsCb) {
  101. queue.push(this.leaveNextTick)
  102. }
  103. }
  104. /**
  105. * The "nextTick" phase of a leaving transition.
  106. */
  107. p.leaveNextTick = function () {
  108. var type = this.getCssTransitionType(this.leaveClass)
  109. if (type) {
  110. var event = type === TYPE_TRANSITION
  111. ? transitionEndEvent
  112. : animationEndEvent
  113. this.setupCssCb(event, this.leaveDone)
  114. } else {
  115. this.leaveDone()
  116. }
  117. }
  118. /**
  119. * The "cleanup" phase of a leaving transition.
  120. */
  121. p.leaveDone = function () {
  122. this.op()
  123. removeClass(this.el, this.leaveClass)
  124. this.callHook('afterLeave')
  125. if (this.cb) this.cb()
  126. }
  127. /**
  128. * Cancel any pending callbacks from a previously running
  129. * but not finished transition.
  130. */
  131. p.cancelPending = function () {
  132. this.op = this.cb = null
  133. var hasPending = false
  134. if (this.pendingCssCb) {
  135. hasPending = true
  136. _.off(this.el, this.pendingCssEvent, this.pendingCssCb)
  137. this.pendingCssEvent = this.pendingCssCb = null
  138. }
  139. if (this.pendingJsCb) {
  140. hasPending = true
  141. this.pendingJsCb.cancel()
  142. this.pendingJsCb = null
  143. }
  144. if (hasPending) {
  145. removeClass(this.el, this.enterClass)
  146. removeClass(this.el, this.leaveClass)
  147. }
  148. if (this.jsCancel) {
  149. this.jsCancel.call(null)
  150. this.jsCancel = null
  151. }
  152. }
  153. /**
  154. * Call a user-provided synchronous hook function.
  155. *
  156. * @param {String} type
  157. */
  158. p.callHook = function (type) {
  159. if (this.hooks && this.hooks[type]) {
  160. this.hooks[type].call(this.vm, this.el)
  161. }
  162. }
  163. /**
  164. * Call a user-provided, potentially-async hook function.
  165. * We check for the length of arguments to see if the hook
  166. * expects a `done` callback. If true, the transition's end
  167. * will be determined by when the user calls that callback;
  168. * otherwise, the end is determined by the CSS transition or
  169. * animation.
  170. *
  171. * @param {String} type
  172. */
  173. p.callHookWithCb = function (type) {
  174. var hook = this.hooks && this.hooks[type]
  175. if (hook) {
  176. if (hook.length > 1) {
  177. this.pendingJsCb = _.cancellable(this[type + 'Done'])
  178. }
  179. this.jsCancel = hook.call(this.vm, this.el, this.pendingJsCb)
  180. }
  181. }
  182. /**
  183. * Get an element's transition type based on the
  184. * calculated styles.
  185. *
  186. * @param {String} className
  187. * @return {Number}
  188. */
  189. p.getCssTransitionType = function (className) {
  190. // skip CSS transitions if page is not visible -
  191. // this solves the issue of transitionend events not
  192. // firing until the page is visible again.
  193. // pageVisibility API is supported in IE10+, same as
  194. // CSS transitions.
  195. /* istanbul ignore if */
  196. if (!transitionEndEvent || (doc && doc.hidden)) {
  197. return
  198. }
  199. var type = this.typeCache[className]
  200. if (type) return type
  201. var inlineStyles = this.el.style
  202. var computedStyles = window.getComputedStyle(this.el)
  203. var transDuration =
  204. inlineStyles[transDurationProp] ||
  205. computedStyles[transDurationProp]
  206. if (transDuration && transDuration !== '0s') {
  207. type = TYPE_TRANSITION
  208. } else {
  209. var animDuration =
  210. inlineStyles[animDurationProp] ||
  211. computedStyles[animDurationProp]
  212. if (animDuration && animDuration !== '0s') {
  213. type = TYPE_ANIMATION
  214. }
  215. }
  216. if (type) {
  217. this.typeCache[className] = type
  218. }
  219. return type
  220. }
  221. /**
  222. * Setup a CSS transitionend/animationend callback.
  223. *
  224. * @param {String} event
  225. * @param {Function} cb
  226. */
  227. p.setupCssCb = function (event, cb) {
  228. this.pendingCssEvent = event
  229. var self = this
  230. var el = this.el
  231. var onEnd = this.pendingCssCb = function (e) {
  232. if (e.target === el) {
  233. _.off(el, event, onEnd)
  234. self.pendingCssEvent = self.pendingCssCb = null
  235. if (!self.pendingJsCb && cb) {
  236. cb()
  237. }
  238. }
  239. }
  240. _.on(el, event, onEnd)
  241. }
  242. module.exports = Transition