Browse Source

support move transitions in transition-group

dev
Evan You 8 years ago
parent
commit
45a489ba81
  1. 110
      src/platforms/web/runtime/components/transition-group.js
  2. 3
      src/platforms/web/runtime/components/transition.js
  3. 115
      src/platforms/web/runtime/modules/transition.js
  4. 111
      src/platforms/web/runtime/transition-util.js
  5. 2
      test/unit/features/component/component-keep-alive.spec.js
  6. 2
      test/unit/features/transition/transition-mode.spec.js
  7. 2
      test/unit/features/transition/transition.spec.js

110
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
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'

115
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
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
}

2
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()

2
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', () => {

2
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', () => {

Loading…
Cancel
Save