Przeglądaj źródła

test for transition

Evan You 11 lat temu
rodzic
commit
50ec33ae75

+ 17 - 15
src/api/dom.js

@@ -97,7 +97,7 @@ exports.$remove = function (cb, withTransition) {
     op = withTransition === false
       ? remove
       : transition.remove
-    op(this.$el, realCb, this)
+    op(this.$el, this, realCb)
   }
 }
 
@@ -117,14 +117,13 @@ function insert (vm, target, op, targetIsDetached, cb) {
     !targetIsDetached &&
     !vm._isAttached &&
     !_.inDoc(vm.$el)
-  var realCb = function () {
-    if (shouldCallHook) vm._callHook('attached')
-    if (cb) cb()
-  }
   if (vm._isBlock) {
-    blockOp(vm, target, op, realCb)
+    blockOp(vm, target, op, cb)
   } else {
-    op(vm.$el, target, realCb, vm)
+    op(vm.$el, target, vm, cb)
+  }
+  if (shouldCallHook) {
+    vm._callHook('attached')
   }
 }
 
@@ -144,10 +143,10 @@ function blockOp (vm, target, op, cb) {
   var next
   while (next !== end) {
     next = current.nextSibling
-    op(current, target, null, vm)
+    op(current, target, vm)
     current = next
   }
-  op(end, target, cb, vm)
+  op(end, target, vm, cb)
 }
 
 /**
@@ -167,10 +166,11 @@ function query (el) {
  *
  * @param {Node} el
  * @param {Node} target
- * @param {Function} cb
+ * @param {Vue} vm - unused
+ * @param {Function} [cb]
  */
 
-function append (el, target, cb) {
+function append (el, target, vm, cb) {
   target.appendChild(el)
   if (cb) cb()
 }
@@ -180,10 +180,11 @@ function append (el, target, cb) {
  *
  * @param {Node} el
  * @param {Node} target
- * @param {Function} cb
+ * @param {Vue} vm - unused
+ * @param {Function} [cb]
  */
 
-function before (el, target, cb) {
+function before (el, target, vm, cb) {
   _.before(el, target)
   if (cb) cb()
 }
@@ -192,10 +193,11 @@ function before (el, target, cb) {
  * Remove operation that takes a callback.
  *
  * @param {Node} el
- * @param {Function} cb
+ * @param {Vue} vm - unused
+ * @param {Function} [cb]
  */
 
-function remove (el, cb) {
+function remove (el, vm, cb) {
   _.remove(el)
   if (cb) cb()
 }

+ 0 - 6
src/batcher.js

@@ -6,7 +6,6 @@ var _ = require('./util')
  */
 
 function Batcher () {
-  this._preFlush = null
   this.reset()
 }
 
@@ -42,14 +41,9 @@ p.push = function (job) {
 
 /**
  * Flush the queue and run the jobs.
- * Will call a preFlush hook if has one.
  */
 
 p.flush = function () {
-  // before flush hook
-  if (this._preFlush) {
-    this._preFlush()
-  }
   // do not cache length because more jobs might be pushed
   // as we run existing jobs
   for (var i = 0; i < this.queue.length; i++) {

+ 60 - 42
src/transition/css.js

@@ -1,16 +1,22 @@
 var _ = require('../util')
-var Batcher = require('../batcher')
-var batcher = new Batcher()
 var transDurationProp = _.transitionProp + 'Duration'
 var animDurationProp = _.animationProp + 'Duration'
 
 /**
- * Force layout before triggering transitions/animations
+ * Force layout before triggering transitions/animations.
+ * This function ensures we only do it once per event loop.
  */
 
-batcher._preFlush = function () {
-  /* jshint unused: false */
-  var f = document.body.offsetHeight
+var forcedLayout = false
+function forceLayout () {
+  if (!forcedLayout) {
+    /* jshint unused: false */
+    var f = document.documentElement.offsetHeight
+    forcedLayout = true
+    _.nextTick(function () {
+      forcedLayout = false
+    })
+  }
 }
 
 /**
@@ -24,11 +30,20 @@ batcher._preFlush = function () {
  */
 
 function getTransitionType (el) {
-  var styles = window.getComputedStyle(el)
-  if (styles[transDurationProp] !== '0s') {
+  var inlineStyles = el.style
+  var computedStyles = window.getComputedStyle(el)
+  var transDuration =
+    inlineStyles[transDurationProp] ||
+    computedStyles[transDurationProp]
+  if (transDuration && transDuration !== '0s') {
     return 1
-  } else if (styles[animDurationProp] !== '0s') {
-    return 2
+  } else {
+    var animDuration =
+      inlineStyles[animDurationProp] ||
+      computedStyles[animDurationProp]
+    if (animDuration && animDuration !== '0s') {
+      return 2
+    }
   }
 }
 
@@ -41,38 +56,40 @@ function getTransitionType (el) {
  * @param {Object} data - target element's transition data
  */
 
-module.exports = function (el, direction, op, data) {
+module.exports = function (el, direction, op, data, cb) {
   var classList = el.classList
-  var callback = data.callback
   var prefix = data.id || 'v'
   var enterClass = prefix + '-enter'
   var leaveClass = prefix + '-leave'
-  // clean up potential previously running transitions
+  // clean up potential previous unfinished transition
   if (data.callback) {
-    _.off(el, data.event, callback)
+    _.off(el, data.event, data.callback)
     classList.remove(enterClass)
     classList.remove(leaveClass)
     data.event = data.callback = null
   }
   var transitionType, onEnd, endEvent
-
   if (direction > 0) { // enter
-
+    // Enter Transition
     classList.add(enterClass)
     op()
     transitionType = getTransitionType(el)
     if (transitionType === 1) {
-      // Enter Transition
-      //
-      // We need to force a reflow to have the enterClass
-      // applied before removing it to trigger the
-      // transition, so they are batched to make sure
-      // there's only one reflow for everything.
-      batcher.push({
-        run: function () {
-          classList.remove(enterClass)
+      forceLayout()
+      classList.remove(enterClass)
+      // only listen for transition end if user has sent
+      // in a callback
+      if (cb) {
+        endEvent = data.event = _.transitionEndEvent
+        onEnd = data.callback = function transitionCb (e) {
+          if (e.target === el) {
+            _.off(el, endEvent, onEnd)
+            data.event = data.callback = null
+            cb()
+          }
         }
-      })
+        _.on(el, endEvent, onEnd)
+      }
     } else if (transitionType === 2) {
       // Enter Animation
       //
@@ -80,48 +97,49 @@ module.exports = function (el, direction, op, data) {
       // element is inserted into the DOM, so we just
       // listen for the animationend event.
       endEvent = data.event = _.animationEndEvent
-      onEnd = data.callback = function (e) {
+      onEnd = data.callback = function transitionCb (e) {
         if (e.target === el) {
           _.off(el, endEvent, onEnd)
           data.event = data.callback = null
           classList.remove(enterClass)
+          if (cb) cb()
         }
       }
       _.on(el, endEvent, onEnd)
+    } else {
+      // no transition applicable
+      classList.remove(enterClass)
+      if (cb) cb()
     }
 
   } else { // leave
 
+    // we need to add the class here before we can sniff
+    // the transition type, and before that we need to
+    // force a layout so the element picks up all transition
+    // css rules.
+    forceLayout()
+    classList.add(leaveClass)
     transitionType = getTransitionType(el)
-    if (
-      transitionType &&
-      (el.offsetWidth || el.offsetHeight)
-    ) {
-      // Leave Transition/Animation
-      //
-      // We push it to the batcher to ensure it triggers
-      // in the same frame with other enter transitions
-      // happening at the same time.
-      batcher.push({
-        run: function () {
-          classList.add(leaveClass)
-        }
-      })
+    if (transitionType) {
       endEvent = data.event = transitionType === 1
         ? _.transitionEndEvent
         : _.animationEndEvent
-      onEnd = data.callback = function (e) {
+      onEnd = data.callback = function transitionCb (e) {
         if (e.target === el) {
           _.off(el, endEvent, onEnd)
           data.event = data.callback = null
           // actually remove node here
           op()
           classList.remove(leaveClass)
+          if (cb) cb()
         }
       }
       _.on(el, endEvent, onEnd)
     } else {
       op()
+      classList.remove(leaveClass)
+      if (cb) cb()
     }
 
   }

+ 22 - 20
src/transition/index.js

@@ -7,15 +7,14 @@ var applyJSTransition = require('./js')
  *
  * @oaram {Element} el
  * @param {Element} target
- * @param {Function} [cb]
  * @param {Vue} vm
+ * @param {Function} [cb]
  */
 
-exports.append = function (el, target, cb, vm) {
+exports.append = function (el, target, vm, cb) {
   apply(el, 1, function () {
     target.appendChild(el)
-    if (cb) cb()
-  }, vm)
+  }, vm, cb)
 }
 
 /**
@@ -23,30 +22,28 @@ exports.append = function (el, target, cb, vm) {
  *
  * @oaram {Element} el
  * @param {Element} target
- * @param {Function} [cb]
  * @param {Vue} vm
+ * @param {Function} [cb]
  */
 
-exports.before = function (el, target, cb, vm) {
+exports.before = function (el, target, vm, cb) {
   apply(el, 1, function () {
     _.before(el, target)
-    if (cb) cb()
-  }, vm)
+  }, vm, cb)
 }
 
 /**
  * Remove with transition.
  *
  * @oaram {Element} el
- * @param {Function} [cb]
  * @param {Vue} vm
+ * @param {Function} [cb]
  */
 
-exports.remove = function (el, cb, vm) {
+exports.remove = function (el, vm, cb) {
   apply(el, -1, function () {
     _.remove(el)
-    if (cb) cb()
-  }, vm)
+  }, vm, cb)
 }
 
 /**
@@ -55,15 +52,14 @@ exports.remove = function (el, cb, vm) {
  *
  * @oaram {Element} el
  * @param {Element} target
- * @param {Function} [cb]
  * @param {Vue} vm
+ * @param {Function} [cb]
  */
 
-exports.removeThenAppend = function (el, target, cb, vm) {
+exports.removeThenAppend = function (el, target, vm, cb) {
   apply(el, -1, function () {
     target.appendChild(el)
-    if (cb) cb()
-  }, vm)
+  }, vm, cb)
 }
 
 /**
@@ -75,9 +71,10 @@ exports.removeThenAppend = function (el, target, cb, vm) {
  *                 -1: leave
  * @param {Function} op - the actual DOM operation
  * @param {Vue} vm
+ * @param {Function} [cb]
  */
 
-var apply = exports.apply = function (el, direction, op, vm) {
+var apply = exports.apply = function (el, direction, op, vm, cb) {
   var transData = el.__v_trans
   if (
     !transData ||
@@ -87,7 +84,9 @@ var apply = exports.apply = function (el, direction, op, vm) {
     // animation.
     (vm.$parent && !vm.$parent._isCompiled)
   ) {
-    return op()
+    op()
+    if (cb) cb()
+    return
   }
   // determine the transition type on the element
   var jsTransition = vm.$options.transitions[transData.id]
@@ -98,7 +97,8 @@ var apply = exports.apply = function (el, direction, op, vm) {
       direction,
       op,
       transData,
-      jsTransition
+      jsTransition,
+      cb
     )
   } else if (_.transitionEndEvent) {
     // css
@@ -106,10 +106,12 @@ var apply = exports.apply = function (el, direction, op, vm) {
       el,
       direction,
       op,
-      transData
+      transData,
+      cb
     )
   } else {
     // not applicable
     op()
+    if (cb) cb()
   }
 }

+ 8 - 2
src/transition/js.js

@@ -6,9 +6,10 @@
  * @param {Function} op - the actual DOM operation
  * @param {Object} data - target element's transition data
  * @param {Object} def - transition definition object
+ * @param {Function} [cb]
  */
 
-module.exports = function (el, direction, op, data, def) {
+module.exports = function (el, direction, op, data, def, cb) {
   if (data.cancel) {
     data.cancel()
     data.cancel = null
@@ -21,16 +22,21 @@ module.exports = function (el, direction, op, data, def) {
     if (def.enter) {
       data.cancel = def.enter(el, function () {
         data.cancel = null
+        if (cb) cb()
       })
+    } else if (cb) {
+      cb()
     }
   } else { // leave
     if (def.leave) {
       data.cancel = def.leave(el, function () {
-        op()
         data.cancel = null
+        op()
+        if (cb) cb()
       })
     } else {
       op()
+      if (cb) cb()
     }
   }
 }

+ 0 - 9
test/unit/specs/batcher_spec.js

@@ -53,13 +53,4 @@ describe('Batcher', function () {
     })
   })
 
-  it('preFlush hook', function (done) {
-    batcher._preFlush = spy
-    batcher.push({ run: function () {}})
-    nextTick(function () {
-      expect(spy.calls.count()).toBe(1)
-      done()
-    })
-  })
-
 })

+ 339 - 0
test/unit/specs/transition_spec.js

@@ -0,0 +1,339 @@
+var Vue = require('../../../src/vue')
+var _ = require('../../../src/util')
+var transition = require('../../../src/transition')
+
+if (_.inBrowser && !_.isIE9) {
+  describe('Transition', function () {
+
+    describe('Wrapper methods', function () {
+      
+      var spy, el, target, parent, vm
+      beforeEach(function () {
+        el = document.createElement('div')
+        target = document.createElement('div')
+        parent = document.createElement('div')
+        parent.appendChild(target)
+        spy = jasmine.createSpy('transition skip')
+        vm = new Vue()
+        spyOn(transition, 'apply')
+      })
+
+      it('append', function () {
+        transition.append(el, parent, vm, spy)
+        expect(parent.lastChild).toBe(el)
+        expect(spy).toHaveBeenCalled()
+      })
+
+      it('before', function () {
+        transition.before(el, target, vm, spy)
+        expect(parent.firstChild).toBe(el)
+        expect(el.nextSibling).toBe(target)
+        expect(spy).toHaveBeenCalled()
+      })
+
+      it('remove', function () {
+        transition.remove(target, vm, spy)
+        expect(parent.childNodes.length).toBe(0)
+        expect(spy).toHaveBeenCalled()
+      })
+
+      it('removeThenAppend', function () {
+        transition.removeThenAppend(target, el, vm, spy)
+        expect(parent.childNodes.length).toBe(0)
+        expect(el.firstChild).toBe(target)
+        expect(spy).toHaveBeenCalled()
+      })
+
+    })
+
+    describe('Skipping', function () {
+
+      var el, vm, op, cb
+      beforeEach(function () {
+        el = document.createElement('div')
+        op = jasmine.createSpy('transition skip op')
+        cb = jasmine.createSpy('transition skip cb')
+        vm = new Vue()
+      })
+      
+      it('skip el with no transition data', function () {
+        transition.apply(el, 1, op, vm, cb)
+        expect(op).toHaveBeenCalled()
+        expect(cb).toHaveBeenCalled()
+      })
+
+      it('skip vm still being compiled', function () {
+        el.__v_trans = { id: 'test' }
+        transition.apply(el, 1, op, vm, cb)
+        expect(op).toHaveBeenCalled()
+        expect(cb).toHaveBeenCalled()
+      })
+
+      it('skip vm with parent still being compiled', function () {
+        el.__v_trans = { id: 'test' }
+        var child = vm.$addChild({
+          el: el
+        })
+        expect(child._isCompiled).toBe(true)
+        transition.apply(el, 1, op, child, cb)
+        expect(op).toHaveBeenCalled()
+        expect(cb).toHaveBeenCalled()
+      })
+
+      it('skip when no transition available', function () {
+        var e = _.transitionEndEvent
+        _.transitionEndEvent = null
+        el.__v_trans = { id: 'test' }
+        vm.$mount(el)
+        transition.apply(el, 1, op, vm, cb)
+        expect(op).toHaveBeenCalled()
+        expect(cb).toHaveBeenCalled()
+        _.transitionEndEvent = e
+      })
+
+    })
+
+    describe('CSS transitions', function () {
+
+      // insert a test css
+      function insertCSS (text) {
+        var cssEl = document.createElement('style')
+        cssEl.textContent = text
+        document.head.appendChild(cssEl)
+      }
+
+      insertCSS('.test {transition: opacity 16ms ease; -webkit-transition: opacity 20ms ease;}')
+      insertCSS('.test-enter, .test-leave { opacity: 0; }')
+      insertCSS(
+        '.test-anim-enter {\
+          animation: test-enter 16ms;\
+          -webkit-animation: test-enter 16ms;}\
+        .test-anim-leave {\
+          animation: test-leave 16ms;\
+          -webkit-animation: test-leave 16ms;}\
+        @keyframes test-enter {\
+          from { opacity: 0 }\
+          to { opacity: 1 }}\
+        @-webkit-keyframes test-enter {\
+          from { opacity: 0 }\
+          to { opacity: 1 }}\
+        @keyframes test-leave {\
+          from { opacity: 1 }\
+          to { opacity: 0 }}\
+        @-webkit-keyframes test-leave {\
+          from { opacity: 1 }\
+          to { opacity: 0 }}'
+      )
+
+      var vm, el, op, cb
+      beforeEach(function (done) {
+        el = document.createElement('div')
+        el.__v_trans = {}
+        vm = new Vue({ el: el })
+        op = jasmine.createSpy('css op')
+        cb = jasmine.createSpy('css cb')
+        document.body.appendChild(el)
+        // !IMPORTANT!
+        // this ensures we force a layout for every test.
+        _.nextTick(done)
+      })
+
+      afterEach(function () {
+        document.body.removeChild(el)
+      })
+
+      it('skip on 0s duration', function () {
+        el.__v_trans.id = 'test'
+        el.style.transition =
+        el.style.WebkitTransition = 'opacity 0s ease'
+        transition.apply(el, 1, op, vm, cb)
+        expect(op).toHaveBeenCalled()
+        expect(cb).toHaveBeenCalled()
+        expect(el.classList.contains('test-enter')).toBe(false)
+        transition.apply(el, -1, op, vm, cb)
+        expect(op.calls.count()).toBe(2)
+        expect(cb.calls.count()).toBe(2)
+        expect(el.classList.contains('test-leave')).toBe(false)
+      })
+
+      it('skip when no transition available', function () {
+        el.__v_trans.id = 'test-no-trans'
+        transition.apply(el, 1, op, vm, cb)
+        expect(op).toHaveBeenCalled()
+        expect(cb).toHaveBeenCalled()
+        expect(el.classList.contains('test-no-trans-enter')).toBe(false)
+        transition.apply(el, -1, op, vm, cb)
+        expect(op.calls.count()).toBe(2)
+        expect(cb.calls.count()).toBe(2)
+        expect(el.classList.contains('test-no-trans-leave')).toBe(false)
+      })
+
+      it('transition enter', function (done) {
+        el.__v_trans.id = 'test'
+        // inline style
+        el.style.transition =
+        el.style.WebkitTransition = 'opacity .2s ease'
+        transition.apply(el, 1, op, vm, cb)
+        expect(op).toHaveBeenCalled()
+        expect(cb).not.toHaveBeenCalled()
+        expect(el.classList.contains('test-enter')).toBe(false)
+        _.on(el, _.transitionEndEvent, function () {
+          expect(cb).toHaveBeenCalled()
+          done()
+        })
+      })
+
+      it('transition leave', function (done) {
+        el.__v_trans.id = 'test'
+        // cascaded class style
+        el.classList.add('test')
+        transition.apply(el, -1, op, vm, cb)
+        expect(op).not.toHaveBeenCalled()
+        expect(cb).not.toHaveBeenCalled()
+        expect(el.classList.contains('test-leave')).toBe(true)
+        _.on(el, _.transitionEndEvent, function () {
+          expect(op).toHaveBeenCalled()
+          expect(cb).toHaveBeenCalled()
+          expect(el.classList.contains('test-leave')).toBe(false)
+          done()
+        })
+      })
+
+      it('animation enter', function (done) {
+        el.__v_trans.id = 'test-anim'
+        transition.apply(el, 1, op, vm, cb)
+        expect(op).toHaveBeenCalled()
+        expect(cb).not.toHaveBeenCalled()
+        expect(el.classList.contains('test-anim-enter')).toBe(true)
+        _.on(el, _.animationEndEvent, function () {
+          expect(el.classList.contains('test-anim-enter')).toBe(false)
+          expect(cb).toHaveBeenCalled()
+          done()
+        })
+      })
+
+      it('animation leave', function (done) {
+        el.__v_trans.id = 'test-anim'
+        transition.apply(el, -1, op, vm, cb)
+        expect(op).not.toHaveBeenCalled()
+        expect(cb).not.toHaveBeenCalled()
+        expect(el.classList.contains('test-anim-leave')).toBe(true)
+        _.on(el, _.animationEndEvent, function () {
+          expect(op).toHaveBeenCalled()
+          expect(cb).toHaveBeenCalled()
+          expect(el.classList.contains('test-anim-leave')).toBe(false)
+          done()
+        })
+      })
+
+      it('clean up unfinished callback', function (done) {
+        el.__v_trans.id = 'test'
+        el.classList.add('test')
+        transition.apply(el, -1, op, vm, cb)
+        expect(el.__v_trans.callback).toBeTruthy()
+        expect(el.classList.contains('test-leave')).toBe(true)
+        _.nextTick(function () {
+          transition.apply(el, 1, op, vm)
+          expect(cb).not.toHaveBeenCalled()
+          expect(el.classList.contains('test-leave')).toBe(false)
+          expect(el.__v_trans.callback).toBeNull()
+          done()
+        })
+      })
+
+    })
+
+    describe('JavaScript transitions', function () {
+
+      var el, vm, op, cb, def, emitter
+      beforeEach(function () {
+        emitter = {}
+        el = document.createElement('div')
+        el.__v_trans = { id: 'test' }
+        document.body.appendChild(el)
+        op = jasmine.createSpy('js transition op')
+        cb = jasmine.createSpy('js transition cb')
+        def = {}
+        vm = new Vue({
+          el: el,
+          transitions: {
+            test: def
+          }
+        })
+      })
+
+      afterEach(function () {
+        document.body.removeChild(el)
+      })
+
+      it('beforeEnter', function () {
+        def.beforeEnter = jasmine.createSpy('js transition beforeEnter')
+        transition.apply(el, 1, op, vm, cb)
+        expect(def.beforeEnter).toHaveBeenCalledWith(el)
+      })
+
+      it('enter', function () {
+        var spy = jasmine.createSpy('js enter')
+        def.enter = function (e, done) {
+          expect(e).toBe(el)
+          expect(op).toHaveBeenCalled()
+          done()
+          expect(cb).toHaveBeenCalled()
+          spy()
+        }
+        transition.apply(el, 1, op, vm, cb)
+        expect(spy).toHaveBeenCalled()
+      })
+
+      it('leave', function () {
+        var spy = jasmine.createSpy('js leave')
+        def.leave = function (e, done) {
+          expect(e).toBe(el)
+          done()
+          expect(op).toHaveBeenCalled()
+          expect(cb).toHaveBeenCalled()
+          spy()
+        }
+        transition.apply(el, -1, op, vm, cb)
+        expect(spy).toHaveBeenCalled()
+      })
+
+      it('no def', function () {
+        transition.apply(el, 1, op, vm, cb)
+        expect(op).toHaveBeenCalled()
+        expect(cb).toHaveBeenCalled()
+        transition.apply(el, -1, op, vm, cb)
+        expect(op.calls.count()).toBe(2)
+        expect(cb.calls.count()).toBe(2)
+      })
+
+      it('optional cleanup callback', function (done) {
+        var cleanupSpy = jasmine.createSpy('js cleanup')
+        var leaveSpy = jasmine.createSpy('js leave')
+        def.enter = function (el, done) {
+          var to = setTimeout(done, 10)
+          return function () {
+            clearTimeout(to)
+            cleanupSpy()
+          }
+        }
+        def.leave = function (el, done) {
+          expect(cleanupSpy).toHaveBeenCalled()
+          leaveSpy()
+          done()
+        }
+        transition.apply(el, 1, op, vm, cb)
+        setTimeout(function () {
+          transition.apply(el, -1, op, vm)
+          expect(leaveSpy).toHaveBeenCalled()
+          setTimeout(function () {
+            expect(cb).not.toHaveBeenCalled()
+            done()
+          }, 10)
+        }, 1)
+      })
+
+    })
+
+  })
+}