import { pushJob } from './queue'
import {
on,
off,
bind,
addClass,
removeClass,
cancellable,
transitionEndEvent,
animationEndEvent,
transitionProp,
animationProp,
warn,
inBrowser
} from '../util/index'
const TYPE_TRANSITION = 'transition'
const TYPE_ANIMATION = 'animation'
const transDurationProp = transitionProp + 'Duration'
const animDurationProp = animationProp + 'Duration'
/**
* If a just-entered element is applied the
* leave class while its enter transition hasn't started yet,
* and the transitioned property has the same value for both
* enter/leave, then the leave transition will be skipped and
* the transitionend event never fires. This function ensures
* its callback to be called after a transition has started
* by waiting for double raf.
*
* It falls back to setTimeout on devices that support CSS
* transitions but not raf (e.g. Android 4.2 browser) - since
* these environments are usually slow, we are giving it a
* relatively large timeout.
*/
const raf = inBrowser && window.requestAnimationFrame
const waitForTransitionStart = raf
/* istanbul ignore next */
? function (fn) { raf(function () { raf(fn) }) }
: function (fn) { setTimeout(fn, 50) }
/**
* A Transition object that encapsulates the state and logic
* of the transition.
*
* @param {Element} el
* @param {String} id
* @param {Object} hooks
* @param {Vue} vm
*/
export default function Transition (el, id, hooks, vm) {
this.id = id
this.el = el
this.enterClass = (hooks && hooks.enterClass) || id + '-enter'
this.leaveClass = (hooks && hooks.leaveClass) || id + '-leave'
this.hooks = hooks
this.vm = vm
// async state
this.pendingCssEvent =
this.pendingCssCb =
this.cancel =
this.pendingJsCb =
this.op =
this.cb = null
this.justEntered = false
this.entered = this.left = false
this.typeCache = {}
// check css transition type
this.type = hooks && hooks.type
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production') {
if (
this.type &&
this.type !== TYPE_TRANSITION &&
this.type !== TYPE_ANIMATION
) {
warn(
'invalid CSS transition type for transition="' +
this.id + '": ' + this.type,
vm
)
}
}
// 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.
*
* 1. enter transition triggered
* 2. call beforeEnter hook
* 3. add enter class
* 4. insert/show element
* 5. call enter hook (with possible explicit js callback)
* 6. reflow
* 7. based on transition type:
* - transition:
* remove class now, wait for transitionend,
* then done if there's no explicit js callback.
* - animation:
* wait for animationend, remove class,
* then done if there's no explicit js callback.
* - no css transition:
* done now if there's no explicit js callback.
* 8. wait for either done or js callback, then call
* afterEnter hook.
*
* @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.entered = false
this.callHookWithCb('enter')
if (this.entered) {
return // user called done synchronously.
}
this.cancel = this.hooks && this.hooks.enterCancelled
pushJob(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 () {
// prevent transition skipping
this.justEntered = true
waitForTransitionStart(() => {
this.justEntered = false
})
var enterDone = this.enterDone
var type = this.getCssTransitionType(this.enterClass)
if (!this.pendingJsCb) {
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 {
enterDone()
}
} else if (type === TYPE_TRANSITION) {
removeClass(this.el, this.enterClass)
}
}
/**
* The "cleanup" phase of an entering transition.
*/
p.enterDone = function () {
this.entered = true
this.cancel = this.pendingJsCb = null
removeClass(this.el, this.enterClass)
this.callHook('afterEnter')
if (this.cb) this.cb()
}
/**
* Start a leaving transition.
*
* 1. leave transition triggered.
* 2. call beforeLeave hook
* 3. add leave class (trigger css transition)
* 4. call leave hook (with possible explicit js callback)
* 5. reflow if no explicit js callback is provided
* 6. based on transition type:
* - transition or animation:
* wait for end event, remove class, then done if
* there's no explicit js callback.
* - no css transition:
* done if there's no explicit js callback.
* 7. wait for either done or js callback, then call
* afterLeave hook.
*
* @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.left = false
this.callHookWithCb('leave')
if (this.left) {
return // user called done synchronously.
}
this.cancel = this.hooks && this.hooks.leaveCancelled
// only need to handle leaveDone if
// 1. the transition is already done (synchronously called
// by the user, which causes this.op set to null)
// 2. there's no explicit js callback
if (this.op && !this.pendingJsCb) {
// if a CSS transition leaves immediately after enter,
// the transitionend event never fires. therefore we
// detect such cases and end the leave immediately.
if (this.justEntered) {
this.leaveDone()
} else {
pushJob(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.left = true
this.cancel = this.pendingJsCb = null
this.op()
removeClass(this.el, this.leaveClass)
this.callHook('afterLeave')
if (this.cb) this.cb()
this.op = null
}
/**
* 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.cancel) {
this.cancel.call(this.vm, this.el)
this.cancel = 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'])
}
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) {
/* istanbul ignore if */
if (
!transitionEndEvent ||
// 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.
document.hidden ||
// explicit js-only transition
(this.hooks && this.hooks.css === false) ||
// element is hidden
isHidden(this.el)
) {
return
}
var type = this.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)
}
/**
* Check if an element is hidden - in that case we can just
* skip the transition alltogether.
*
* @param {Element} el
* @return {Boolean}
*/
function isHidden (el) {
if (/svg$/.test(el.namespaceURI)) {
// SVG elements do not have offset(Width|Height)
// so we need to check the client rect
var rect = el.getBoundingClientRect()
return !(rect.width || rect.height)
} else {
return !(
el.offsetWidth ||
el.offsetHeight ||
el.getClientRects().length
)
}
}
|