From 45a489ba81628b984da1a44102d7e09a32ad5ab3 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 13 Jul 2016 21:49:09 -0400 Subject: [PATCH] support move transitions in transition-group --- .../runtime/components/transition-group.js | 110 +++++++++++++++-- .../web/runtime/components/transition.js | 3 + .../web/runtime/modules/transition.js | 115 ++---------------- src/platforms/web/runtime/transition-util.js | 111 +++++++++++++++++ .../component/component-keep-alive.spec.js | 2 +- .../transition/transition-mode.spec.js | 2 +- .../features/transition/transition.spec.js | 2 +- 7 files changed, 221 insertions(+), 124 deletions(-) create mode 100644 src/platforms/web/runtime/transition-util.js diff --git a/src/platforms/web/runtime/components/transition-group.js b/src/platforms/web/runtime/components/transition-group.js index d06b4ce6..70b5528a 100644 --- a/src/platforms/web/runtime/components/transition-group.js +++ b/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) + } } } diff --git a/src/platforms/web/runtime/components/transition.js b/src/platforms/web/runtime/components/transition.js index 43553135..18963cad 100644 --- a/src/platforms/web/runtime/components/transition.js +++ b/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' diff --git a/src/platforms/web/runtime/modules/transition.js b/src/platforms/web/runtime/modules/transition.js index 1753685d..430fde2c 100644 --- a/src/platforms/web/runtime/modules/transition.js +++ b/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, durations: Array): 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 () => { diff --git a/src/platforms/web/runtime/transition-util.js b/src/platforms/web/runtime/transition-util.js new file mode 100644 index 00000000..9546bea7 --- /dev/null +++ b/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, durations: Array): 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 +} diff --git a/test/unit/features/component/component-keep-alive.spec.js b/test/unit/features/component/component-keep-alive.spec.js index 6ed1e0e6..ec0d8774 100644 --- a/test/unit/features/component/component-keep-alive.spec.js +++ b/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() diff --git a/test/unit/features/transition/transition-mode.spec.js b/test/unit/features/transition/transition-mode.spec.js index dfb0f9e5..fb8336fd 100644 --- a/test/unit/features/transition/transition-mode.spec.js +++ b/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', () => { diff --git a/test/unit/features/transition/transition.spec.js b/test/unit/features/transition/transition.spec.js index 9a5dcfdd..c1e1c873 100644 --- a/test/unit/features/transition/transition.spec.js +++ b/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', () => {