var _ = require('../util') var queue = require('./queue') var addClass = _.addClass var removeClass = _.removeClass var transitionEndEvent = _.transitionEndEvent var animationEndEvent = _.animationEndEvent var transDurationProp = _.transitionProp + 'Duration' var animDurationProp = _.animationProp + 'Duration' var doc = typeof document === 'undefined' ? null : document var TYPE_TRANSITION = 1 var TYPE_ANIMATION = 2 /** * A Transition object that encapsulates the state and logic * of the transition. * * @param {Element} el * @param {String} id * @param {Object} hooks * @param {Vue} vm */ function Transition (el, id, hooks, vm) { this.el = el this.enterClass = id + '-enter' this.leaveClass = id + '-leave' this.hooks = hooks this.vm = vm // async state this.pendingCssEvent = this.pendingCssCb = this.jsCancel = this.pendingJsCb = this.op = this.cb = null this.typeCache = {} // bind var self = this ;['enterNextTick', 'enterDone', 'leaveNextTick', 'leaveDone'] .forEach(function (m) { self[m] = _.bind(self[m], self) }) } var p = Transition.prototype /** * Start an entering transition. * * @param {Function} op - insert/show the element * @param {Function} [cb] */ p.enter = function (op, cb) { this.cancelPending() this.callHook('beforeEnter') this.cb = cb addClass(this.el, this.enterClass) op() this.callHookWithCb('enter') queue.push(this.enterNextTick) } /** * The "nextTick" phase of an entering transition, which is * to be pushed into a queue and executed after a reflow so * that removing the class can trigger a CSS transition. */ p.enterNextTick = function () { var type = this.getCssTransitionType(this.enterClass) var enterDone = this.enterDone if (type === TYPE_TRANSITION) { // trigger transition by removing enter class now removeClass(this.el, this.enterClass) this.setupCssCb(transitionEndEvent, enterDone) } else if (type === TYPE_ANIMATION) { this.setupCssCb(animationEndEvent, enterDone) } else if (!this.pendingJsCb) { enterDone() } } /** * The "cleanup" phase of an entering transition. */ p.enterDone = function () { this.jsCancel = this.pendingJsCb = null removeClass(this.el, this.enterClass) this.callHook('afterEnter') if (this.cb) this.cb() } /** * Start a leaving transition. * * @param {Function} op - remove/hide the element * @param {Function} [cb] */ p.leave = function (op, cb) { this.cancelPending() this.callHook('beforeLeave') this.op = op this.cb = cb addClass(this.el, this.leaveClass) this.callHookWithCb('leave') // only need to do leaveNextTick if there's no explicit // js callback if (!this.pendingJsCb) { queue.push(this.leaveNextTick) } } /** * The "nextTick" phase of a leaving transition. */ p.leaveNextTick = function () { var type = this.getCssTransitionType(this.leaveClass) if (type) { var event = type === TYPE_TRANSITION ? transitionEndEvent : animationEndEvent this.setupCssCb(event, this.leaveDone) } else { this.leaveDone() } } /** * The "cleanup" phase of a leaving transition. */ p.leaveDone = function () { this.op() removeClass(this.el, this.leaveClass) this.callHook('afterLeave') if (this.cb) this.cb() } /** * Cancel any pending callbacks from a previously running * but not finished transition. */ p.cancelPending = function () { this.op = this.cb = null var hasPending = false if (this.pendingCssCb) { hasPending = true _.off(this.el, this.pendingCssEvent, this.pendingCssCb) this.pendingCssEvent = this.pendingCssCb = null } if (this.pendingJsCb) { hasPending = true this.pendingJsCb.cancel() this.pendingJsCb = null } if (hasPending) { removeClass(this.el, this.enterClass) removeClass(this.el, this.leaveClass) } if (this.jsCancel) { this.jsCancel.call(null) this.jsCancel = null } } /** * Call a user-provided synchronous hook function. * * @param {String} type */ p.callHook = function (type) { if (this.hooks && this.hooks[type]) { this.hooks[type].call(this.vm, this.el) } } /** * Call a user-provided, potentially-async hook function. * We check for the length of arguments to see if the hook * expects a `done` callback. If true, the transition's end * will be determined by when the user calls that callback; * otherwise, the end is determined by the CSS transition or * animation. * * @param {String} type */ p.callHookWithCb = function (type) { var hook = this.hooks && this.hooks[type] if (hook) { if (hook.length > 1) { this.pendingJsCb = _.cancellable(this[type + 'Done']) } this.jsCancel = hook.call(this.vm, this.el, this.pendingJsCb) } } /** * Get an element's transition type based on the * calculated styles. * * @param {String} className * @return {Number} */ p.getCssTransitionType = function (className) { // skip CSS transitions if page is not visible - // this solves the issue of transitionend events not // firing until the page is visible again. // pageVisibility API is supported in IE10+, same as // CSS transitions. /* istanbul ignore if */ if (!transitionEndEvent || (doc && doc.hidden)) { return } var type = this.typeCache[className] if (type) return type var inlineStyles = this.el.style var computedStyles = window.getComputedStyle(this.el) var transDuration = inlineStyles[transDurationProp] || computedStyles[transDurationProp] if (transDuration && transDuration !== '0s') { type = TYPE_TRANSITION } else { var animDuration = inlineStyles[animDurationProp] || computedStyles[animDurationProp] if (animDuration && animDuration !== '0s') { type = TYPE_ANIMATION } } if (type) { this.typeCache[className] = type } return type } /** * Setup a CSS transitionend/animationend callback. * * @param {String} event * @param {Function} cb */ p.setupCssCb = function (event, cb) { this.pendingCssEvent = event var self = this var el = this.el var onEnd = this.pendingCssCb = function (e) { if (e.target === el) { _.off(el, event, onEnd) self.pendingCssEvent = self.pendingCssCb = null if (!self.pendingJsCb && cb) { cb() } } } _.on(el, event, onEnd) } module.exports = Transition