Skip to content

Commit 45a489b

Browse files
committed
support move transitions in transition-group
1 parent 940ebf2 commit 45a489b

File tree

7 files changed

+221
-124
lines changed

7 files changed

+221
-124
lines changed
Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { warn, extend } from 'core/util/index'
2-
import { transitionProps, extractTransitionData } from './transition'
1+
// Provides transition support for list items.
2+
// supports move transitions using the FLIP technique.
33

44
// Because the vdom's children update algorithm is "unstable" - i.e.
55
// it doesn't guarantee the relative positioning of removed elements,
@@ -9,18 +9,29 @@ import { transitionProps, extractTransitionData } from './transition'
99
// into the final disired state. This way in the second pass removed
1010
// nodes will remain where they should be.
1111

12-
export default {
13-
props: extend({ tag: String }, transitionProps),
12+
import { warn, extend } from 'core/util/index'
13+
import { transitionProps, extractTransitionData } from './transition'
14+
import {
15+
hasTransition,
16+
addTransitionClass,
17+
removeTransitionClass,
18+
getTransitionInfo,
19+
transitionEndEvent
20+
} from '../transition-util'
1421

15-
beforeUpdate () {
16-
// force removing pass
17-
this.__patch__(this._vnode, this.kept)
18-
this._vnode = this.kept
19-
},
22+
const props = extend({
23+
tag: String,
24+
moveClass: String
25+
}, transitionProps)
26+
27+
delete props.mode
28+
29+
export default {
30+
props,
2031

2132
render (h) {
22-
const prevMap = this.prevChildrenMap
23-
const map = this.prevChildrenMap = {}
33+
const prevMap = this.map
34+
const map = this.map = {}
2435
const rawChildren = this.$slots.default || []
2536
const children = []
2637
const kept = []
@@ -33,7 +44,10 @@ export default {
3344
children.push(c)
3445
map[c.key] = c
3546
;(c.data || (c.data = {})).transition = transitionData
36-
if (prevMap && prevMap[c.key]) {
47+
const prev = prevMap && prevMap[c.key]
48+
if (prev) {
49+
prev.data.kept = true
50+
c.data.pos = prev.elm.getBoundingClientRect()
3751
kept.push(c)
3852
}
3953
} else if (process.env.NODE_ENV !== 'production') {
@@ -47,10 +61,80 @@ export default {
4761
}
4862

4963
const tag = this.tag || this.$vnode.data.tag || 'span'
50-
if (this._isMounted) {
64+
if (prevMap) {
5165
this.kept = h(tag, null, kept)
66+
this.removed = []
67+
for (const key in prevMap) {
68+
const c = prevMap[key]
69+
if (!c.data.kept) {
70+
c.data.pos = c.elm.getBoundingClientRect()
71+
this.removed.push(c)
72+
}
73+
}
5274
}
5375

5476
return h(tag, null, children)
77+
},
78+
79+
beforeUpdate () {
80+
// force removing pass
81+
this.__patch__(this._vnode, this.kept)
82+
this._vnode = this.kept
83+
},
84+
85+
updated () {
86+
const children = this.kept.children.concat(this.removed)
87+
const moveClass = this.moveClass || (this.name + '-move')
88+
if (!children.length || !this.hasMove(children[0].elm, moveClass)) {
89+
return
90+
}
91+
92+
children.forEach(c => {
93+
const oldPos = c.data.pos
94+
const newPos = c.elm.getBoundingClientRect()
95+
const dx = oldPos.left - newPos.left
96+
const dy = oldPos.top - newPos.top
97+
if (dx || dy) {
98+
c.data.moved = true
99+
const s = c.elm.style
100+
s.transform = s.WebkitTransform = `translate(${dx}px,${dy}px)`
101+
s.transitionDuration = '0s'
102+
}
103+
})
104+
105+
// force reflow to put everything in position
106+
const f = document.body.offsetHeight // eslint-disable-line
107+
108+
children.forEach(c => {
109+
if (c.data.moved) {
110+
const el = c.elm
111+
const s = el.style
112+
addTransitionClass(el, moveClass)
113+
s.transform = s.WebkitTransform = s.transitionDuration = ''
114+
if (el._pendingMoveCb) {
115+
el.removeEventListener(transitionEndEvent, el._pendingMoveCb)
116+
}
117+
el.addEventListener(transitionEndEvent, el._pendingMoveCb = function cb () {
118+
el.removeEventListener(transitionEndEvent, cb)
119+
el._pendingMoveCb = null
120+
removeTransitionClass(el, moveClass)
121+
})
122+
}
123+
})
124+
},
125+
126+
methods: {
127+
hasMove (el, moveClass) {
128+
if (!hasTransition) {
129+
return false
130+
}
131+
if (this._hasMove != null) {
132+
return this._hasMove
133+
}
134+
addTransitionClass(el, moveClass)
135+
const info = getTransitionInfo(el)
136+
removeTransitionClass(el, moveClass)
137+
return (this._hasMove = info.hasTransform)
138+
}
55139
}
56140
}

src/platforms/web/runtime/components/transition.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
// Provides transition support for a single element/component.
2+
// supports transition mode (out-in / in-out)
3+
14
import { warn } from 'core/util/index'
25
import { noop, camelize } from 'shared/util'
36
import { getRealChild, mergeVNodeHook } from 'core/vdom/helpers'

src/platforms/web/runtime/modules/transition.js

Lines changed: 7 additions & 108 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,14 @@
11
/* @flow */
22

33
import { inBrowser } from 'core/util/index'
4-
import { isIE9 } from 'web/util/index'
5-
import { addClass, removeClass } from '../class-util'
6-
import { cached, remove, extend } from 'shared/util'
4+
import { cached, extend } from 'shared/util'
75
import { mergeVNodeHook } from 'core/vdom/helpers'
8-
9-
const hasTransition = inBrowser && !isIE9
10-
const TRANSITION = 'transition'
11-
const ANIMATION = 'animation'
12-
13-
// Transition property/event sniffing
14-
export let transitionProp = 'transition'
15-
export let transitionEndEvent = 'transitionend'
16-
export let animationProp = 'animation'
17-
export let animationEndEvent = 'animationend'
18-
if (hasTransition) {
19-
/* istanbul ignore if */
20-
if (window.ontransitionend === undefined &&
21-
window.onwebkittransitionend !== undefined) {
22-
transitionProp = 'WebkitTransition'
23-
transitionEndEvent = 'webkitTransitionEnd'
24-
}
25-
if (window.onanimationend === undefined &&
26-
window.onwebkitanimationend !== undefined) {
27-
animationProp = 'WebkitAnimation'
28-
animationEndEvent = 'webkitAnimationEnd'
29-
}
30-
}
31-
32-
const raf = (inBrowser && window.requestAnimationFrame) || setTimeout
33-
export function nextFrame (fn: Function) {
34-
raf(() => {
35-
raf(fn)
36-
})
37-
}
6+
import {
7+
nextFrame,
8+
addTransitionClass,
9+
removeTransitionClass,
10+
whenTransitionEnds
11+
} from '../transition-util'
3812

3913
export function enter (vnode: VNodeWithData) {
4014
const el: any = vnode.elm
@@ -234,81 +208,6 @@ const autoCssTransition: (name: string) => Object = cached(name => {
234208
}
235209
})
236210

237-
function addTransitionClass (el: any, cls: string) {
238-
(el._transitionClasses || (el._transitionClasses = [])).push(cls)
239-
addClass(el, cls)
240-
}
241-
242-
function removeTransitionClass (el: any, cls: string) {
243-
if (el._transitionClasses) {
244-
remove(el._transitionClasses, cls)
245-
}
246-
removeClass(el, cls)
247-
}
248-
249-
function whenTransitionEnds (el: Element, cb: Function) {
250-
const { type, timeout, propCount } = getTransitionInfo(el)
251-
if (!type) return cb()
252-
const event = type === TRANSITION ? transitionEndEvent : animationEndEvent
253-
let ended = 0
254-
const end = () => {
255-
el.removeEventListener(event, onEnd)
256-
cb()
257-
}
258-
const onEnd = () => {
259-
if (++ended >= propCount) {
260-
end()
261-
}
262-
}
263-
setTimeout(() => {
264-
if (ended < propCount) {
265-
end()
266-
}
267-
}, timeout + 1)
268-
el.addEventListener(event, onEnd)
269-
}
270-
271-
function getTransitionInfo (el: Element): {
272-
type: ?string,
273-
propCount: number,
274-
timeout: number
275-
} {
276-
const styles = window.getComputedStyle(el)
277-
// 1. determine the maximum duration (timeout)
278-
const transitioneDelays = styles[transitionProp + 'Delay'].split(', ')
279-
const transitionDurations = styles[transitionProp + 'Duration'].split(', ')
280-
const animationDelays = styles[animationProp + 'Delay'].split(', ')
281-
const animationDurations = styles[animationProp + 'Duration'].split(', ')
282-
const transitionTimeout = getTimeout(transitioneDelays, transitionDurations)
283-
const animationTimeout = getTimeout(animationDelays, animationDurations)
284-
const timeout = Math.max(transitionTimeout, animationTimeout)
285-
const type = timeout > 0
286-
? transitionTimeout > animationTimeout
287-
? TRANSITION
288-
: ANIMATION
289-
: null
290-
const propCount = type
291-
? type === TRANSITION
292-
? transitionDurations.length
293-
: animationDurations.length
294-
: 0
295-
return {
296-
type,
297-
timeout,
298-
propCount
299-
}
300-
}
301-
302-
function getTimeout (delays: Array<string>, durations: Array<string>): number {
303-
return Math.max.apply(null, durations.map((d, i) => {
304-
return toMs(d) + toMs(delays[i])
305-
}))
306-
}
307-
308-
function toMs (s: string): number {
309-
return Number(s.slice(0, -1)) * 1000
310-
}
311-
312211
function once (fn: Function): Function {
313212
let called = false
314213
return () => {
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
import { inBrowser } from 'core/util/index'
2+
import { isIE9 } from 'web/util/index'
3+
import { remove } from 'shared/util'
4+
import { addClass, removeClass } from './class-util'
5+
6+
export const hasTransition = inBrowser && !isIE9
7+
const TRANSITION = 'transition'
8+
const ANIMATION = 'animation'
9+
10+
// Transition property/event sniffing
11+
export let transitionProp = 'transition'
12+
export let transitionEndEvent = 'transitionend'
13+
export let animationProp = 'animation'
14+
export let animationEndEvent = 'animationend'
15+
if (hasTransition) {
16+
/* istanbul ignore if */
17+
if (window.ontransitionend === undefined &&
18+
window.onwebkittransitionend !== undefined) {
19+
transitionProp = 'WebkitTransition'
20+
transitionEndEvent = 'webkitTransitionEnd'
21+
}
22+
if (window.onanimationend === undefined &&
23+
window.onwebkitanimationend !== undefined) {
24+
animationProp = 'WebkitAnimation'
25+
animationEndEvent = 'webkitAnimationEnd'
26+
}
27+
}
28+
29+
const raf = (inBrowser && window.requestAnimationFrame) || setTimeout
30+
export function nextFrame (fn: Function) {
31+
raf(() => {
32+
raf(fn)
33+
})
34+
}
35+
36+
export function addTransitionClass (el: any, cls: string) {
37+
(el._transitionClasses || (el._transitionClasses = [])).push(cls)
38+
addClass(el, cls)
39+
}
40+
41+
export function removeTransitionClass (el: any, cls: string) {
42+
if (el._transitionClasses) {
43+
remove(el._transitionClasses, cls)
44+
}
45+
removeClass(el, cls)
46+
}
47+
48+
export function whenTransitionEnds (el: Element, cb: Function) {
49+
const { type, timeout, propCount } = getTransitionInfo(el)
50+
if (!type) return cb()
51+
const event = type === TRANSITION ? transitionEndEvent : animationEndEvent
52+
let ended = 0
53+
const end = () => {
54+
el.removeEventListener(event, onEnd)
55+
cb()
56+
}
57+
const onEnd = () => {
58+
if (++ended >= propCount) {
59+
end()
60+
}
61+
}
62+
setTimeout(() => {
63+
if (ended < propCount) {
64+
end()
65+
}
66+
}, timeout + 1)
67+
el.addEventListener(event, onEnd)
68+
}
69+
70+
const transformRE = /\b(transform|all)(,|$)/
71+
72+
export function getTransitionInfo (el: Element): {
73+
type: ?string,
74+
propCount: number,
75+
timeout: number
76+
} {
77+
const styles = window.getComputedStyle(el)
78+
const transitionProps = styles[transitionProp + 'Property']
79+
const transitioneDelays = styles[transitionProp + 'Delay'].split(', ')
80+
const transitionDurations = styles[transitionProp + 'Duration'].split(', ')
81+
const animationDelays = styles[animationProp + 'Delay'].split(', ')
82+
const animationDurations = styles[animationProp + 'Duration'].split(', ')
83+
const transitionTimeout = getTimeout(transitioneDelays, transitionDurations)
84+
const animationTimeout = getTimeout(animationDelays, animationDurations)
85+
const timeout = Math.max(transitionTimeout, animationTimeout)
86+
const type = timeout > 0
87+
? transitionTimeout > animationTimeout
88+
? TRANSITION
89+
: ANIMATION
90+
: null
91+
return {
92+
type,
93+
timeout,
94+
propCount: type
95+
? type === TRANSITION
96+
? transitionDurations.length
97+
: animationDurations.length
98+
: 0,
99+
hasTransform: type === TRANSITION && transformRE.test(transitionProps)
100+
}
101+
}
102+
103+
function getTimeout (delays: Array<string>, durations: Array<string>): number {
104+
return Math.max.apply(null, durations.map((d, i) => {
105+
return toMs(d) + toMs(delays[i])
106+
}))
107+
}
108+
109+
function toMs (s: string): number {
110+
return Number(s.slice(0, -1)) * 1000
111+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import Vue from 'vue'
22
import injectStyles from '../transition/inject-styles'
33
import { isIE9 } from 'web/util/index'
4-
import { nextFrame } from 'web/runtime/modules/transition'
4+
import { nextFrame } from 'web/runtime/transition-util'
55

66
describe('Component keep-alive', () => {
77
const duration = injectStyles()

0 commit comments

Comments
 (0)