2
0
Evan You 10 жил өмнө
parent
commit
79bfb1809a

+ 40 - 15
src/platforms/web/runtime/modules/transition.js

@@ -2,7 +2,7 @@
 
 import { addClass, removeClass } from '../class-util'
 import { inBrowser, resolveAsset } from 'core/util/index'
-import { cached, remove } from 'shared/util'
+import { cached, remove, extend } from 'shared/util'
 import { isIE9 } from 'web/util/index'
 
 const hasTransition = inBrowser && !isIE9
@@ -65,6 +65,9 @@ export function enter (vnode: VNodeWithData) {
       removeTransitionClass(el, enterActiveClass)
     }
     if (cb.cancelled) {
+      if (enterClass) {
+        removeTransitionClass(el, enterClass)
+      }
       enterCancelled && enterCancelled(el)
     } else {
       afterEnter && afterEnter(el)
@@ -81,9 +84,11 @@ export function enter (vnode: VNodeWithData) {
   }
   if (enterActiveClass) {
     nextFrame(() => {
-      addTransitionClass(el, enterActiveClass)
-      if (!userWantsControl) {
-        whenTransitionEnds(el, cb)
+      if (!cb.cancelled) {
+        addTransitionClass(el, enterActiveClass)
+        if (!userWantsControl) {
+          whenTransitionEnds(el, cb)
+        }
       }
     })
   }
@@ -120,6 +125,9 @@ export function leave (vnode: VNodeWithData, rm: Function) {
       removeTransitionClass(el, leaveActiveClass)
     }
     if (cb.cancelled) {
+      if (leaveClass) {
+        removeTransitionClass(el, leaveClass)
+      }
       leaveCancelled && leaveCancelled(el)
     } else {
       rm()
@@ -137,9 +145,11 @@ export function leave (vnode: VNodeWithData, rm: Function) {
   }
   if (leaveActiveClass) {
     nextFrame(() => {
-      addTransitionClass(el, leaveActiveClass)
-      if (!userWantsControl) {
-        whenTransitionEnds(el, cb)
+      if (!cb.cancelled) {
+        addTransitionClass(el, leaveActiveClass)
+        if (!userWantsControl) {
+          whenTransitionEnds(el, cb)
+        }
       }
     })
   }
@@ -150,13 +160,28 @@ export function leave (vnode: VNodeWithData, rm: Function) {
 }
 
 function resolveTransition (id: string | Object, context: Component): Object {
-  let definition = id && typeof id === 'string'
-    ? resolveAsset(context.$options, 'transitions', id) || id
-    : id
-  if (definition === true) definition = 'v'
-  return typeof definition === 'string'
-    ? autoCssTransition(definition)
-    : definition
+  if (id && typeof id === 'string') {
+    const def = resolveAsset(context.$options, 'transitions', id)
+    if (def) {
+      return ensureTransitionClasses(id, def)
+    } else {
+      return autoCssTransition(id)
+    }
+  } else if (typeof id === 'object') { // inline transition object
+    return ensureTransitionClasses('v', id)
+  } else {
+    return autoCssTransition('v')
+  }
+}
+
+function ensureTransitionClasses (id: string, def: Object): Object {
+  if (def.css === false) return def
+  const key = `_cache_${id}`
+  if (def[key]) return def[key]
+  const res = def[key] = {}
+  extend(res, autoCssTransition(id))
+  extend(res, def)
+  return res
 }
 
 const autoCssTransition: (name: string) => Object = cached(name => {
@@ -196,7 +221,7 @@ function whenTransitionEnds (el: Element, cb: Function) {
     if (ended < propCount) {
       end()
     }
-  }, timeout)
+  }, timeout + 1)
   el.addEventListener(event, onEnd)
 }
 

+ 0 - 79
test/unit/features/transition/transition-css.spec.js

@@ -1,79 +0,0 @@
-import Vue from 'vue'
-import { isIE9 } from 'web/util/index'
-import { nextFrame } from 'web/runtime/modules/transition'
-
-if (!isIE9) {
-  describe('Transition CSS', () => {
-    const duration = 50
-    insertCSS(`
-      .test {
-        -webkit-transition: opacity ${duration}ms ease;
-        transition: opacity ${duration}ms ease;
-      }
-      .test-enter, .test-leave-active {
-        opacity: 0;
-      }
-      .test-anim-enter {
-        animation: test-enter ${duration}ms;
-        -webkit-animation: test-enter ${duration}ms;
-      }
-      .test-anim-leave {
-        animation: test-leave ${duration}ms;
-        -webkit-animation: test-leave ${duration}ms;
-      }
-      @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 }
-      }
-    `)
-
-    it('basic transitions', done => {
-      const el = document.createElement('div')
-      document.body.appendChild(el)
-      const vm = new Vue({
-        template: '<div><div v-if="ok" class="test" transition="test">foo</div></div>',
-        data: { ok: true }
-      }).$mount(el)
-
-      // should not apply transition on initial render by default
-      expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
-      vm.ok = false
-      waitForUpdate(() => {
-        expect(vm.$el.children[0].className).toBe('test test-leave')
-      }).thenWaitFor(nextFrame).then(() => {
-        expect(vm.$el.children[0].className).toBe('test test-leave-active')
-      }).thenWaitFor(timeout(duration + 1)).then(() => {
-        expect(vm.$el.children.length).toBe(0)
-        vm.ok = true
-      }).then(() => {
-        expect(vm.$el.children[0].className).toBe('test test-enter')
-      }).thenWaitFor(nextFrame).then(() => {
-        expect(vm.$el.children[0].className).toBe('test test-enter-active')
-      }).thenWaitFor(timeout(duration + 1)).then(() => {
-        expect(vm.$el.children[0].className).toBe('test')
-      }).then(done)
-    })
-  })
-}
-
-function insertCSS (text) {
-  var cssEl = document.createElement('style')
-  cssEl.textContent = text.trim()
-  document.head.appendChild(cssEl)
-}
-
-function timeout (n) {
-  return next => setTimeout(next, n)
-}

+ 0 - 0
test/unit/features/transition/transition-js.spec.js


+ 0 - 0
test/unit/features/transition/transition-mixed.spec.js


+ 315 - 0
test/unit/features/transition/transition.spec.js

@@ -0,0 +1,315 @@
+import Vue from 'vue'
+import { isIE9 } from 'web/util/index'
+import { nextFrame } from 'web/runtime/modules/transition'
+
+if (!isIE9) {
+  describe('Transition system', () => {
+    const duration = 30
+    insertCSS(`
+      .test {
+        -webkit-transition: opacity ${duration}ms ease;
+        transition: opacity ${duration}ms ease;
+      }
+      .test-enter, .test-leave-active, .hello, .bye.active {
+        opacity: 0;
+      }
+      .test-anim-enter-active {
+        animation: test-enter ${duration}ms;
+        -webkit-animation: test-enter ${duration}ms;
+      }
+      .test-anim-leave-active {
+        animation: test-leave ${duration}ms;
+        -webkit-animation: test-leave ${duration}ms;
+      }
+      @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 }
+      }
+    `)
+
+    let el
+    beforeEach(() => {
+      el = document.createElement('div')
+      document.body.appendChild(el)
+    })
+
+    it('basic transitions', done => {
+      const vm = new Vue({
+        template: '<div><div v-if="ok" class="test" transition="test">foo</div></div>',
+        data: { ok: true }
+      }).$mount(el)
+
+      // should not apply transition on initial render by default
+      expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
+      vm.ok = false
+      waitForUpdate(() => {
+        expect(vm.$el.children[0].className).toBe('test test-leave')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test test-leave-active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(1)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(vm.$el.children.length).toBe(0)
+        vm.ok = true
+      }).then(() => {
+        expect(vm.$el.children[0].className).toBe('test test-enter')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test test-enter-active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(0)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(vm.$el.children[0].className).toBe('test')
+      }).then(done)
+    })
+
+    it('custom transition classes', done => {
+      const vm = new Vue({
+        template: '<div><div v-if="ok" class="test" transition="test">foo</div></div>',
+        data: { ok: true },
+        transitions: {
+          test: {
+            enterClass: 'hello',
+            enterActiveClass: 'hello active',
+            leaveClass: 'bye',
+            leaveActiveClass: 'bye active'
+          }
+        }
+      }).$mount(el)
+
+      // should not apply transition on initial render by default
+      expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
+      vm.ok = false
+      waitForUpdate(() => {
+        expect(vm.$el.children[0].className).toBe('test bye')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test bye active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(1)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(vm.$el.children.length).toBe(0)
+        vm.ok = true
+      }).then(() => {
+        expect(vm.$el.children[0].className).toBe('test hello')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test hello active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(0)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(vm.$el.children[0].className).toBe('test')
+      }).then(done)
+    })
+
+    it('dynamic transition', done => {
+      const vm = new Vue({
+        template: '<div><div v-if="ok" class="test" :transition="trans">foo</div></div>',
+        data: {
+          ok: true,
+          trans: 'test'
+        }
+      }).$mount(el)
+
+      // should not apply transition on initial render by default
+      expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
+      vm.ok = false
+      waitForUpdate(() => {
+        expect(vm.$el.children[0].className).toBe('test test-leave')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test test-leave-active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(1)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(vm.$el.children.length).toBe(0)
+        vm.ok = true
+        vm.trans = 'changed'
+      }).then(() => {
+        expect(vm.$el.children[0].className).toBe('test changed-enter')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test changed-enter-active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(0)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(vm.$el.children[0].className).toBe('test')
+      }).then(done)
+    })
+
+    it('inline transition object', done => {
+      const vm = new Vue({
+        template: `<div><div v-if="ok" class="test" :transition="{
+          enterClass: 'hello',
+          enterActiveClass: 'hello active',
+          leaveClass: 'bye',
+          leaveActiveClass: 'bye active'
+        }">foo</div></div>`,
+        data: { ok: true }
+      }).$mount(el)
+
+      // should not apply transition on initial render by default
+      expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
+      vm.ok = false
+      waitForUpdate(() => {
+        expect(vm.$el.children[0].className).toBe('test bye')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test bye active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(1)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(vm.$el.children.length).toBe(0)
+        vm.ok = true
+      }).then(() => {
+        expect(vm.$el.children[0].className).toBe('test hello')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test hello active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(0)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(vm.$el.children[0].className).toBe('test')
+      }).then(done)
+    })
+
+    it('transition with JavaScript hooks', done => {
+      const beforeLeaveSpy = jasmine.createSpy('beforeLeave')
+      const beforeEnterSpy = jasmine.createSpy('beforeEnter')
+      const hooks = {
+        beforeLeave: el => {
+          expect(el).toBe(vm.$el.children[0])
+          expect(el.className).toBe('test')
+          beforeLeaveSpy()
+        },
+        leave: jasmine.createSpy('leave'),
+        afterLeave: jasmine.createSpy('afterLeave'),
+        beforeEnter: el => {
+          expect(vm.$el.contains(el)).toBe(false)
+          expect(el.className).toBe('test')
+          beforeEnterSpy()
+        },
+        enter: jasmine.createSpy('enter'),
+        afterEnter: jasmine.createSpy('afterEnter')
+      }
+
+      const vm = new Vue({
+        template: '<div><div v-if="ok" class="test" transition="test">foo</div></div>',
+        data: { ok: true },
+        transitions: {
+          test: hooks
+        }
+      }).$mount(el)
+
+      // should not apply transition on initial render by default
+      expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
+      vm.ok = false
+      waitForUpdate(() => {
+        expect(beforeLeaveSpy).toHaveBeenCalled()
+        expect(hooks.leave).toHaveBeenCalled()
+        expect(vm.$el.children[0].className).toBe('test test-leave')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(hooks.afterLeave).not.toHaveBeenCalled()
+        expect(vm.$el.children[0].className).toBe('test test-leave-active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(1)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(hooks.afterLeave).toHaveBeenCalled()
+        expect(vm.$el.children.length).toBe(0)
+        vm.ok = true
+      }).then(() => {
+        expect(beforeEnterSpy).toHaveBeenCalled()
+        expect(hooks.enter).toHaveBeenCalled()
+        expect(vm.$el.children[0].className).toBe('test test-enter')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(hooks.afterEnter).not.toHaveBeenCalled()
+        expect(vm.$el.children[0].className).toBe('test test-enter-active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(0)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(hooks.afterEnter).toHaveBeenCalled()
+        expect(vm.$el.children[0].className).toBe('test')
+      }).then(done)
+    })
+
+    it('enterCancelled', done => {
+      const spy = jasmine.createSpy('enterCancelled')
+      const vm = new Vue({
+        template: '<div><div v-if="ok" class="test" transition="test">foo</div></div>',
+        data: { ok: false },
+        transitions: {
+          test: {
+            enterCancelled: spy
+          }
+        }
+      }).$mount(el)
+
+      expect(vm.$el.innerHTML).toBe('')
+      vm.ok = true
+      waitForUpdate(() => {
+        expect(vm.$el.children[0].className).toBe('test test-enter')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        vm.ok = false
+      }).then(() => {
+        expect(spy).toHaveBeenCalled()
+        expect(vm.$el.children[0].className).toBe('test test-leave')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test test-leave-active')
+      }).thenWaitFor(timeout(duration + 1)).then(() => {
+        expect(vm.$el.children.length).toBe(0)
+      }).then(done)
+    })
+
+    it('transition with v-show', () => {
+
+    })
+
+    it('leaveCancelled (v-show only)', () => {
+
+    })
+
+    it('animations', done => {
+      const vm = new Vue({
+        template: '<div><div v-if="ok" class="test" transition="test-anim">foo</div></div>',
+        data: { ok: true }
+      }).$mount(el)
+
+      // should not apply transition on initial render by default
+      expect(vm.$el.innerHTML).toBe('<div class="test">foo</div>')
+      vm.ok = false
+      waitForUpdate(() => {
+        expect(vm.$el.children[0].className).toBe('test test-anim-leave')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test test-anim-leave-active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(1)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(vm.$el.children.length).toBe(0)
+        vm.ok = true
+      }).then(() => {
+        expect(vm.$el.children[0].className).toBe('test test-anim-enter')
+      }).thenWaitFor(nextFrame).then(() => {
+        expect(vm.$el.children[0].className).toBe('test test-anim-enter-active')
+      }).thenWaitFor(timeout(duration / 2)).then(() => {
+        expect(window.getComputedStyle(vm.$el.children[0]).opacity).not.toBe(0)
+      }).thenWaitFor(timeout(duration / 2 + 1)).then(() => {
+        expect(vm.$el.children[0].className).toBe('test')
+      }).then(done)
+    })
+  })
+}
+
+function insertCSS (text) {
+  var cssEl = document.createElement('style')
+  cssEl.textContent = text.trim()
+  document.head.appendChild(cssEl)
+}
+
+function timeout (n) {
+  return next => setTimeout(next, n)
+}