Просмотр исходного кода

support move transitions in transition-group

Evan You 10 лет назад
Родитель
Сommit
45a489ba81

+ 97 - 13
src/platforms/web/runtime/components/transition-group.js

@@ -1,5 +1,5 @@
-import { warn, extend } from 'core/util/index'
-import { transitionProps, extractTransitionData } from './transition'
+// Provides transition support for list items.
+// supports move transitions using the FLIP technique.
 
 // Because the vdom's children update algorithm is "unstable" - i.e.
 // it doesn't guarantee the relative positioning of removed elements,
@@ -9,18 +9,29 @@ import { transitionProps, extractTransitionData } from './transition'
 // into the final disired state. This way in the second pass removed
 // nodes will remain where they should be.
 
-export default {
-  props: extend({ tag: String }, transitionProps),
+import { warn, extend } from 'core/util/index'
+import { transitionProps, extractTransitionData } from './transition'
+import {
+  hasTransition,
+  addTransitionClass,
+  removeTransitionClass,
+  getTransitionInfo,
+  transitionEndEvent
+} from '../transition-util'
 
-  beforeUpdate () {
-    // force removing pass
-    this.__patch__(this._vnode, this.kept)
-    this._vnode = this.kept
-  },
+const props = extend({
+  tag: String,
+  moveClass: String
+}, transitionProps)
+
+delete props.mode
+
+export default {
+  props,
 
   render (h) {
-    const prevMap = this.prevChildrenMap
-    const map = this.prevChildrenMap = {}
+    const prevMap = this.map
+    const map = this.map = {}
     const rawChildren = this.$slots.default || []
     const children = []
     const kept = []
@@ -33,7 +44,10 @@ export default {
           children.push(c)
           map[c.key] = c
           ;(c.data || (c.data = {})).transition = transitionData
-          if (prevMap && prevMap[c.key]) {
+          const prev = prevMap && prevMap[c.key]
+          if (prev) {
+            prev.data.kept = true
+            c.data.pos = prev.elm.getBoundingClientRect()
             kept.push(c)
           }
         } else if (process.env.NODE_ENV !== 'production') {
@@ -47,10 +61,80 @@ export default {
     }
 
     const tag = this.tag || this.$vnode.data.tag || 'span'
-    if (this._isMounted) {
+    if (prevMap) {
       this.kept = h(tag, null, kept)
+      this.removed = []
+      for (const key in prevMap) {
+        const c = prevMap[key]
+        if (!c.data.kept) {
+          c.data.pos = c.elm.getBoundingClientRect()
+          this.removed.push(c)
+        }
+      }
     }
 
     return h(tag, null, children)
+  },
+
+  beforeUpdate () {
+    // force removing pass
+    this.__patch__(this._vnode, this.kept)
+    this._vnode = this.kept
+  },
+
+  updated () {
+    const children = this.kept.children.concat(this.removed)
+    const moveClass = this.moveClass || (this.name + '-move')
+    if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
+      return
+    }
+
+    children.forEach(c => {
+      const oldPos = c.data.pos
+      const newPos = c.elm.getBoundingClientRect()
+      const dx = oldPos.left - newPos.left
+      const dy = oldPos.top - newPos.top
+      if (dx || dy) {
+        c.data.moved = true
+        const s = c.elm.style
+        s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
+        s.transitionDuration = '0s'
+      }
+    })
+
+    // force reflow to put everything in position
+    const f = document.body.offsetHeight // eslint-disable-line
+
+    children.forEach(c => {
+      if (c.data.moved) {
+        const el = c.elm
+        const s = el.style
+        addTransitionClass(el, moveClass)
+        s.transform = s.WebkitTransform = s.transitionDuration = ''
+        if (el._pendingMoveCb) {
+          el.removeEventListener(transitionEndEvent, el._pendingMoveCb)
+        }
+        el.addEventListener(transitionEndEvent, el._pendingMoveCb = function cb () {
+          el.removeEventListener(transitionEndEvent, cb)
+          el._pendingMoveCb = null
+          removeTransitionClass(el, moveClass)
+        })
+      }
+    })
+  },
+
+  methods: {
+    hasMove (el, moveClass) {
+      if (!hasTransition) {
+        return false
+      }
+      if (this._hasMove != null) {
+        return this._hasMove
+      }
+      addTransitionClass(el, moveClass)
+      const info = getTransitionInfo(el)
+      removeTransitionClass(el, moveClass)
+      return (this._hasMove = info.hasTransform)
+    }
   }
 }

+ 3 - 0
src/platforms/web/runtime/components/transition.js

@@ -1,3 +1,6 @@
+// Provides transition support for a single element/component.
+// supports transition mode (out-in / in-out)
+
 import { warn } from 'core/util/index'
 import { noop, camelize } from 'shared/util'
 import { getRealChild, mergeVNodeHook } from 'core/vdom/helpers'

+ 7 - 108
src/platforms/web/runtime/modules/transition.js

@@ -1,40 +1,14 @@
 /* @flow */
 
 import { inBrowser } from 'core/util/index'
-import { isIE9 } from 'web/util/index'
-import { addClass, removeClass } from '../class-util'
-import { cached, remove, extend } from 'shared/util'
+import { cached, extend } from 'shared/util'
 import { mergeVNodeHook } from 'core/vdom/helpers'
-
-const hasTransition = inBrowser && !isIE9
-const TRANSITION = 'transition'
-const ANIMATION = 'animation'
-
-// Transition property/event sniffing
-export let transitionProp = 'transition'
-export let transitionEndEvent = 'transitionend'
-export let animationProp = 'animation'
-export let animationEndEvent = 'animationend'
-if (hasTransition) {
-  /* istanbul ignore if */
-  if (window.ontransitionend === undefined &&
-    window.onwebkittransitionend !== undefined) {
-    transitionProp = 'WebkitTransition'
-    transitionEndEvent = 'webkitTransitionEnd'
-  }
-  if (window.onanimationend === undefined &&
-    window.onwebkitanimationend !== undefined) {
-    animationProp = 'WebkitAnimation'
-    animationEndEvent = 'webkitAnimationEnd'
-  }
-}
-
-const raf = (inBrowser && window.requestAnimationFrame) || setTimeout
-export function nextFrame (fn: Function) {
-  raf(() => {
-    raf(fn)
-  })
-}
+import {
+  nextFrame,
+  addTransitionClass,
+  removeTransitionClass,
+  whenTransitionEnds
+} from '../transition-util'
 
 export function enter (vnode: VNodeWithData) {
   const el: any = vnode.elm
@@ -234,81 +208,6 @@ const autoCssTransition: (name: string) => Object = cached(name => {
   }
 })
 
-function addTransitionClass (el: any, cls: string) {
-  (el._transitionClasses || (el._transitionClasses = [])).push(cls)
-  addClass(el, cls)
-}
-
-function removeTransitionClass (el: any, cls: string) {
-  if (el._transitionClasses) {
-    remove(el._transitionClasses, cls)
-  }
-  removeClass(el, cls)
-}
-
-function whenTransitionEnds (el: Element, cb: Function) {
-  const { type, timeout, propCount } = getTransitionInfo(el)
-  if (!type) return cb()
-  const event = type === TRANSITION ? transitionEndEvent : animationEndEvent
-  let ended = 0
-  const end = () => {
-    el.removeEventListener(event, onEnd)
-    cb()
-  }
-  const onEnd = () => {
-    if (++ended >= propCount) {
-      end()
-    }
-  }
-  setTimeout(() => {
-    if (ended < propCount) {
-      end()
-    }
-  }, timeout + 1)
-  el.addEventListener(event, onEnd)
-}
-
-function getTransitionInfo (el: Element): {
-  type: ?string,
-  propCount: number,
-  timeout: number
-} {
-  const styles = window.getComputedStyle(el)
-  // 1. determine the maximum duration (timeout)
-  const transitioneDelays = styles[transitionProp + 'Delay'].split(', ')
-  const transitionDurations = styles[transitionProp + 'Duration'].split(', ')
-  const animationDelays = styles[animationProp + 'Delay'].split(', ')
-  const animationDurations = styles[animationProp + 'Duration'].split(', ')
-  const transitionTimeout = getTimeout(transitioneDelays, transitionDurations)
-  const animationTimeout = getTimeout(animationDelays, animationDurations)
-  const timeout = Math.max(transitionTimeout, animationTimeout)
-  const type = timeout > 0
-    ? transitionTimeout > animationTimeout
-      ? TRANSITION
-      : ANIMATION
-    : null
-  const propCount = type
-    ? type === TRANSITION
-      ? transitionDurations.length
-      : animationDurations.length
-    : 0
-  return {
-    type,
-    timeout,
-    propCount
-  }
-}
-
-function getTimeout (delays: Array<string>, durations: Array<string>): number {
-  return Math.max.apply(null, durations.map((d, i) => {
-    return toMs(d) + toMs(delays[i])
-  }))
-}
-
-function toMs (s: string): number {
-  return Number(s.slice(0, -1)) * 1000
-}
-
 function once (fn: Function): Function {
   let called = false
   return () => {

+ 111 - 0
src/platforms/web/runtime/transition-util.js

@@ -0,0 +1,111 @@
+import { inBrowser } from 'core/util/index'
+import { isIE9 } from 'web/util/index'
+import { remove } from 'shared/util'
+import { addClass, removeClass } from './class-util'
+
+export const hasTransition = inBrowser && !isIE9
+const TRANSITION = 'transition'
+const ANIMATION = 'animation'
+
+// Transition property/event sniffing
+export let transitionProp = 'transition'
+export let transitionEndEvent = 'transitionend'
+export let animationProp = 'animation'
+export let animationEndEvent = 'animationend'
+if (hasTransition) {
+  /* istanbul ignore if */
+  if (window.ontransitionend === undefined &&
+    window.onwebkittransitionend !== undefined) {
+    transitionProp = 'WebkitTransition'
+    transitionEndEvent = 'webkitTransitionEnd'
+  }
+  if (window.onanimationend === undefined &&
+    window.onwebkitanimationend !== undefined) {
+    animationProp = 'WebkitAnimation'
+    animationEndEvent = 'webkitAnimationEnd'
+  }
+}
+
+const raf = (inBrowser && window.requestAnimationFrame) || setTimeout
+export function nextFrame (fn: Function) {
+  raf(() => {
+    raf(fn)
+  })
+}
+
+export function addTransitionClass (el: any, cls: string) {
+  (el._transitionClasses || (el._transitionClasses = [])).push(cls)
+  addClass(el, cls)
+}
+
+export function removeTransitionClass (el: any, cls: string) {
+  if (el._transitionClasses) {
+    remove(el._transitionClasses, cls)
+  }
+  removeClass(el, cls)
+}
+
+export function whenTransitionEnds (el: Element, cb: Function) {
+  const { type, timeout, propCount } = getTransitionInfo(el)
+  if (!type) return cb()
+  const event = type === TRANSITION ? transitionEndEvent : animationEndEvent
+  let ended = 0
+  const end = () => {
+    el.removeEventListener(event, onEnd)
+    cb()
+  }
+  const onEnd = () => {
+    if (++ended >= propCount) {
+      end()
+    }
+  }
+  setTimeout(() => {
+    if (ended < propCount) {
+      end()
+    }
+  }, timeout + 1)
+  el.addEventListener(event, onEnd)
+}
+
+const transformRE = /\b(transform|all)(,|$)/
+
+export function getTransitionInfo (el: Element): {
+  type: ?string,
+  propCount: number,
+  timeout: number
+} {
+  const styles = window.getComputedStyle(el)
+  const transitionProps = styles[transitionProp + 'Property']
+  const transitioneDelays = styles[transitionProp + 'Delay'].split(', ')
+  const transitionDurations = styles[transitionProp + 'Duration'].split(', ')
+  const animationDelays = styles[animationProp + 'Delay'].split(', ')
+  const animationDurations = styles[animationProp + 'Duration'].split(', ')
+  const transitionTimeout = getTimeout(transitioneDelays, transitionDurations)
+  const animationTimeout = getTimeout(animationDelays, animationDurations)
+  const timeout = Math.max(transitionTimeout, animationTimeout)
+  const type = timeout > 0
+    ? transitionTimeout > animationTimeout
+      ? TRANSITION
+      : ANIMATION
+    : null
+  return {
+    type,
+    timeout,
+    propCount: type
+      ? type === TRANSITION
+        ? transitionDurations.length
+        : animationDurations.length
+      : 0,
+    hasTransform: type === TRANSITION && transformRE.test(transitionProps)
+  }
+}
+
+function getTimeout (delays: Array<string>, durations: Array<string>): number {
+  return Math.max.apply(null, durations.map((d, i) => {
+    return toMs(d) + toMs(delays[i])
+  }))
+}
+
+function toMs (s: string): number {
+  return Number(s.slice(0, -1)) * 1000
+}

+ 1 - 1
test/unit/features/component/component-keep-alive.spec.js

@@ -1,7 +1,7 @@
 import Vue from 'vue'
 import injectStyles from '../transition/inject-styles'
 import { isIE9 } from 'web/util/index'
-import { nextFrame } from 'web/runtime/modules/transition'
+import { nextFrame } from 'web/runtime/transition-util'
 
 describe('Component keep-alive', () => {
   const duration = injectStyles()

+ 1 - 1
test/unit/features/transition/transition-mode.spec.js

@@ -1,7 +1,7 @@
 import Vue from 'vue'
 import injectStyles from './inject-styles'
 import { isIE9 } from 'web/util/index'
-import { nextFrame } from 'web/runtime/modules/transition'
+import { nextFrame } from 'web/runtime/transition-util'
 
 if (!isIE9) {
   describe('Transition mode', () => {

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

@@ -1,7 +1,7 @@
 import Vue from 'vue'
 import injectStyles from './inject-styles'
 import { isIE9 } from 'web/util/index'
-import { nextFrame } from 'web/runtime/modules/transition'
+import { nextFrame } from 'web/runtime/transition-util'
 
 if (!isIE9) {
   describe('Transition system', () => {