Browse Source

transition refactor

Evan You 11 years ago
parent
commit
5f4388fa78

+ 2 - 2
component.json

@@ -61,9 +61,9 @@
     "src/parsers/path.js",
     "src/parsers/template.js",
     "src/parsers/text.js",
-    "src/transition/css.js",
     "src/transition/index.js",
-    "src/transition/js.js",
+    "src/transition/queue.js",
+    "src/transition/transition.js",
     "src/util/debug.js",
     "src/util/dom.js",
     "src/util/env.js",

+ 7 - 10
src/directives/transition.js

@@ -1,4 +1,5 @@
 var _ = require('../util')
+var Transition = require('../transition/transition')
 
 module.exports = {
 
@@ -12,19 +13,15 @@ module.exports = {
   },
 
   update: function (id, oldId) {
+    var el = this.el
     var vm = this.el.__vue__ || this.vm
-    this.el.__v_trans = {
-      id: id,
-      // resolve the custom transition functions now
-      // so the transition module knows this is a
-      // javascript transition without having to check
-      // computed CSS.
-      hooks: _.resolveAsset(vm.$options, 'transitions', id)
-    }
+    var hooks = _.resolveAsset(vm.$options, 'transitions', id)
+    id = id || 'v'
+    el.__v_trans = new Transition(el, id, hooks, vm)
     if (oldId) {
-      _.removeClass(this.el, oldId + '-transition')
+      _.removeClass(el, oldId + '-transition')
     }
-    _.addClass(this.el, (id || 'v') + '-transition')
+    _.addClass(el, id + '-transition')
   }
 
 }

+ 0 - 272
src/transition/apply.js

@@ -1,272 +0,0 @@
-var _ = require('../util')
-var addClass = _.addClass
-var removeClass = _.removeClass
-var transDurationProp = _.transitionProp + 'Duration'
-var animDurationProp = _.animationProp + 'Duration'
-var doc = typeof document === 'undefined' ? null : document
-
-var TYPE_TRANSITION = 1
-var TYPE_ANIMATION = 2
-
-var queue = []
-var queued = false
-
-/**
- * Push a job into the transition queue, which is to be
- * executed on next frame.
- *
- * @param {Element} el    - target element
- * @param {Number} dir    - 1: enter, -1: leave
- * @param {Function} op   - the actual dom operation
- * @param {String} cls    - the className to remove when the
- *                          transition is done.
- * @param {Vue} vm
- * @param {Function} [cb] - user supplied callback.
- */
-
-function push (el, dir, op, cls, vm, cb) {
-  queue.push({
-    el  : el,
-    dir : dir,
-    cb  : cb,
-    cls : cls,
-    vm  : vm,
-    op  : op
-  })
-  if (!queued) {
-    queued = true
-    _.nextTick(flush)
-  }
-}
-
-/**
- * Flush the queue, and do one forced reflow before
- * triggering transitions.
- */
-
-function flush () {
-  var f = document.documentElement.offsetHeight
-  queue.forEach(run)
-  queue = []
-  queued = false
-  // dummy return, so js linters don't complain about unused
-  // variable f
-  return f
-}
-
-/**
- * Run a transition job.
- *
- * @param {Object} job
- */
-
-function run (job) {
-
-  var el = job.el
-  var data = el.__v_trans
-  var hooks = data.hooks
-  var cls = job.cls
-  var cb = job.cb
-  var op = job.op
-  var vm = job.vm
-  var transitionType = getTransitionType(el, data, cls)
-
-  if (job.dir > 0) { // ENTER
-
-    // call javascript enter hook
-    if (hooks && hooks.enter) {
-      var expectsCb = hooks.enter.length > 1
-      if (expectsCb) {
-        data.hookCb = function () {
-          data.hookCancel = data.hookCb = null
-          if (hooks.afterEnter) {
-            hooks.afterEnter.call(vm, el)
-          }
-          if (cb) cb()
-        }
-      }
-      data.hookCancel = hooks.enter.call(vm, el, data.hookCb)
-    }
-
-    if (transitionType === TYPE_TRANSITION) {
-      // trigger transition by removing enter class
-      removeClass(el, cls)
-      setupTransitionCb(_.transitionEndEvent)
-    } else if (transitionType === TYPE_ANIMATION) {
-      // animations are triggered when class is added
-      // so we just listen for animationend to remove it.
-      setupTransitionCb(_.animationEndEvent, function () {
-        removeClass(el, cls)
-      })
-    } else if (!data.hookCb) {
-      // no transition applicable
-      removeClass(el, cls)
-      if (hooks && hooks.afterEnter) {
-        hooks.afterEnter.call(vm, el)
-      }
-      if (cb) {
-        cb()
-      }
-    }
-
-  } else { // LEAVE
-    // only need to handle leave if there's no hook callback
-    if (!data.hookCb) {
-      if (transitionType) {
-        // leave transitions/animations are both triggered
-        // by adding the class, just remove it on end event.
-        var event = transitionType === TYPE_TRANSITION
-          ? _.transitionEndEvent
-          : _.animationEndEvent
-        setupTransitionCb(event, function () {
-          op()
-          removeClass(el, cls)
-        })
-      } else {
-        op()
-        removeClass(el, cls)
-        if (cb) cb()
-      }
-    }
-  }
-
-  /**
-   * Set up a transition end callback, store the callback
-   * on the element's __v_trans data object, so we can
-   * clean it up if another transition is triggered before
-   * the callback is fired.
-   *
-   * @param {String} event
-   * @param {Function} [cleanupFn]
-   */
-
-  function setupTransitionCb (event, cleanupFn) {
-    data.event = event
-    var onEnd = data.callback = function transitionCb (e) {
-      if (e.target === el) {
-        _.off(el, event, onEnd)
-        data.event = data.callback = null
-        if (cleanupFn) cleanupFn()
-        if (!data.hookCb) {
-          if (job.dir > 0 && hooks && hooks.afterEnter) {
-            hooks.afterEnter.call(vm, el)
-          }
-          if (job.dir < 0 && hooks && hooks.afterLeave) {
-            hooks.afterLeave.call(vm, el)
-          }
-          if (cb) {
-            cb()
-          }
-        }
-      }
-    }
-    _.on(el, event, onEnd)
-  }
-}
-
-/**
- * Get an element's transition type based on the
- * calculated styles
- *
- * @param {Element} el
- * @param {Object} data
- * @param {String} className
- * @return {Number}
- */
-
-function getTransitionType (el, data, 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.
-  if (!_.transitionEndEvent || (doc && doc.hidden)) {
-    return
-  }
-  var type = data.cache && data.cache[className]
-  if (type) return type
-  var inlineStyles = el.style
-  var computedStyles = window.getComputedStyle(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) {
-    if (!data.cache) data.cache = {}
-    data.cache[className] = type
-  }
-  return type
-}
-
-/**
- * Apply CSS transition to an element.
- *
- * @param {Element} el
- * @param {Number} direction - 1: enter, -1: leave
- * @param {Function} op - the actual DOM operation
- * @param {Object} data - target element's transition data
- * @param {Vue} vm
- * @param {Function} cb
- */
-
-module.exports = function (el, direction, op, data, vm, cb) {
-  vm = el.__vue__ || vm
-  var hooks = data.hooks
-  var prefix = data.id || 'v'
-  var enterClass = prefix + '-enter'
-  var leaveClass = prefix + '-leave'
-  // clean up potential previous unfinished transition
-  if (data.callback) {
-    _.off(el, data.event, data.callback)
-    removeClass(el, enterClass)
-    removeClass(el, leaveClass)
-    data.event = data.callback = null
-  }
-  // cancel function from js hooks
-  if (data.hookCancel) {
-    data.hookCancel()
-    data.hookCancel = null
-  }
-  if (direction > 0) { // enter
-    // enter class
-    addClass(el, enterClass)
-    // js hook
-    if (hooks && hooks.beforeEnter) {
-      hooks.beforeEnter.call(vm, el)
-    }
-    op()
-    push(el, direction, null, enterClass, vm, cb)
-  } else { // leave
-    if (hooks && hooks.beforeLeave) {
-      hooks.beforeLeave.call(vm, el)
-    }
-    // add leave class
-    addClass(el, leaveClass)
-    // execute js leave hook
-    if (hooks && hooks.leave) {
-      var expectsCb = hooks.leave.length > 1
-      if (expectsCb) {
-        data.hookCb = function () {
-          data.hookCancel = data.hookCb = null
-          op()
-          removeClass(el, leaveClass)
-          if (hooks && hooks.afterLeave) {
-            hooks.afterLeave.call(vm, el)
-          }
-          if (cb) cb()
-        }
-      }
-      data.hookCancel = hooks.leave.call(vm, el, data.hookCb)
-    }
-    push(el, direction, op, leaveClass, vm, cb)
-  }
-}

+ 4 - 4
src/transition/index.js

@@ -1,5 +1,4 @@
 var _ = require('../util')
-var applyTransition = require('./apply')
 
 /**
  * Append with transition.
@@ -107,9 +106,9 @@ exports.blockRemove = function (start, end, vm) {
  */
 
 var apply = exports.apply = function (el, direction, op, vm, cb) {
-  var transData = el.__v_trans
+  var transition = el.__v_trans
   if (
-    !transData ||
+    !transition ||
     !vm._isCompiled ||
     // if the vm is being manipulated by a parent directive
     // during the parent's compilation phase, skip the
@@ -120,5 +119,6 @@ var apply = exports.apply = function (el, direction, op, vm, cb) {
     if (cb) cb()
     return
   }
-  applyTransition(el, direction, op, transData, vm, cb)
+  var action = direction > 0 ? 'enter' : 'leave'
+  transition[action](op, cb)
 }

+ 29 - 0
src/transition/queue.js

@@ -0,0 +1,29 @@
+var _ = require('../util')
+var queue = []
+var queued = false
+
+exports.push = function (job) {
+  queue.push(job)
+  if (!queued) {
+    queued = true
+    _.nextTick(flush)
+  }
+}
+
+/**
+ * Flush the queue, and do one forced reflow before
+ * triggering transitions.
+ */
+
+function flush () {
+  // Force layout
+  var f = document.documentElement.offsetHeight
+  for (var i = 0; i < queue.length; i++) {
+    queue[i]()
+  }
+  queue = []
+  queued = false
+  // dummy return, so js linters don't complain about
+  // unused variable f
+  return f
+}

+ 194 - 0
src/transition/transition.js

@@ -0,0 +1,194 @@
+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
+
+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
+  ;['nextEnter', 'afterEnter', 'nextLeave', 'afterLeave']
+    .forEach(function (m) {
+      self[m] = _.bind(self[m], self)
+    })
+}
+
+var p = Transition.prototype
+
+p.enter = function (op, cb) {
+  this.cancelPending()
+  this.callHook('beforeEnter')
+  this.cb = cb
+  addClass(this.el, this.enterClass)
+  op()
+  queue.push(this.nextEnter)
+}
+
+p.nextEnter = function () {
+  var enterHook = this.hooks && this.hooks.enter
+  var afterEnter = this.afterEnter
+  var pendingJsCb, expectsCb
+  if (enterHook) {
+    expectsCb = enterHook.length > 1
+    if (expectsCb) {
+      this.pendingJsCb = _.cancellable(afterEnter)
+    }
+    this.jsCancel = enterHook.call(this.vm, this.el, this.pendingJsCb)
+  }
+  var type = this.getCssTransitionType(this.enterClass)
+  if (type === TYPE_TRANSITION) {
+    // trigger transition by removing enter class now
+    removeClass(this.el, this.enterClass)
+    this.setupCssCb(transitionEndEvent, afterEnter)
+  } else if (type === TYPE_ANIMATION) {
+    this.setupCssCb(animationEndEvent, afterEnter)
+  } else if (!expectsCb) {
+    afterEnter()
+  }
+}
+
+p.afterEnter = function () {
+  this.jsCancel = this.pendingJsCb = null
+  removeClass(this.el, this.enterClass)
+  this.callHook('afterEnter')
+  if (this.cb) this.cb()
+}
+
+p.leave = function (op, cb) {
+  this.cancelPending()
+  this.callHook('beforeLeave')
+  this.op = op
+  this.cb = cb
+  addClass(this.el, this.leaveClass)
+  var leaveHook = this.hooks && this.hooks.leave
+  var pendingJsCb, expectsCb
+  if (leaveHook) {
+    expectsCb = leaveHook.length > 1
+    if (expectsCb) {
+      this.pendingJsCb = _.cancellable(this.afterLeave)
+    }
+    this.jsCancel = leaveHook.call(this.vm, this.el, this.pendingJsCb)
+  }
+  // only need to handle leave cb if no js cb is provided
+  if (!expectsCb) {
+    queue.push(this.nextLeave)
+  }
+}
+
+p.nextLeave = function () {
+  var type = this.getCssTransitionType(this.leaveClass)
+  if (type) {
+    var event = type === TYPE_TRANSITION
+      ? transitionEndEvent
+      : animationEndEvent
+    this.setupCssCb(event, this.afterLeave)
+  } else {
+    this.afterLeave()
+  }
+}
+
+p.afterLeave = function () {
+  this.op()
+  removeClass(this.el, this.leaveClass)
+  this.callHook('afterLeave')
+  if (this.cb) this.cb()
+}
+
+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
+  }
+}
+
+p.callHook = function (type) {
+  if (this.hooks && this.hooks[type]) {
+    this.hooks[type].call(this.vm, this.el)
+  }
+}
+
+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.
+  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
+}
+
+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

+ 19 - 0
src/util/lang.js

@@ -247,4 +247,23 @@ exports.indexOf = function (arr, obj) {
     if (arr[i] === obj) return i
   }
   return -1
+}
+
+/**
+ * Make a cancellable version of an async callback.
+ *
+ * @param {Function} fn
+ * @return {Function}
+ */
+
+exports.cancellable = function (fn) {
+  var cb = function () {
+    if (!cb.cancelled) {
+      return fn.apply(this, arguments)
+    }
+  }
+  cb.cancel = function () {
+    cb.cancelled = true
+  }
+  return cb
 }