/* @flow */
|
|
// Provides transition support for a single element/component.
|
// supports transition mode (out-in / in-out)
|
|
import { warn } from 'core/util/index'
|
import { camelize, extend, isPrimitive } from 'shared/util'
|
import {
|
mergeVNodeHook,
|
isAsyncPlaceholder,
|
getFirstComponentChild
|
} from 'core/vdom/helpers/index'
|
|
export const transitionProps = {
|
name: String,
|
appear: Boolean,
|
css: Boolean,
|
mode: String,
|
type: String,
|
enterClass: String,
|
leaveClass: String,
|
enterToClass: String,
|
leaveToClass: String,
|
enterActiveClass: String,
|
leaveActiveClass: String,
|
appearClass: String,
|
appearActiveClass: String,
|
appearToClass: String,
|
duration: [Number, String, Object]
|
}
|
|
// in case the child is also an abstract component, e.g. <keep-alive>
|
// we want to recursively retrieve the real component to be rendered
|
function getRealChild (vnode: ?VNode): ?VNode {
|
const compOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
|
if (compOptions && compOptions.Ctor.options.abstract) {
|
return getRealChild(getFirstComponentChild(compOptions.children))
|
} else {
|
return vnode
|
}
|
}
|
|
export function extractTransitionData (comp: Component): Object {
|
const data = {}
|
const options: ComponentOptions = comp.$options
|
// props
|
for (const key in options.propsData) {
|
data[key] = comp[key]
|
}
|
// events.
|
// extract listeners and pass them directly to the transition methods
|
const listeners: ?Object = options._parentListeners
|
for (const key in listeners) {
|
data[camelize(key)] = listeners[key]
|
}
|
return data
|
}
|
|
function placeholder (h: Function, rawChild: VNode): ?VNode {
|
if (/\d-keep-alive$/.test(rawChild.tag)) {
|
return h('keep-alive', {
|
props: rawChild.componentOptions.propsData
|
})
|
}
|
}
|
|
function hasParentTransition (vnode: VNode): ?boolean {
|
while ((vnode = vnode.parent)) {
|
if (vnode.data.transition) {
|
return true
|
}
|
}
|
}
|
|
function isSameChild (child: VNode, oldChild: VNode): boolean {
|
return oldChild.key === child.key && oldChild.tag === child.tag
|
}
|
|
const isNotTextNode = (c: VNode) => c.tag || isAsyncPlaceholder(c)
|
|
const isVShowDirective = d => d.name === 'show'
|
|
export default {
|
name: 'transition',
|
props: transitionProps,
|
abstract: true,
|
|
render (h: Function) {
|
let children: any = this.$slots.default
|
if (!children) {
|
return
|
}
|
|
// filter out text nodes (possible whitespaces)
|
children = children.filter(isNotTextNode)
|
/* istanbul ignore if */
|
if (!children.length) {
|
return
|
}
|
|
// warn multiple elements
|
if (process.env.NODE_ENV !== 'production' && children.length > 1) {
|
warn(
|
'<transition> can only be used on a single element. Use ' +
|
'<transition-group> for lists.',
|
this.$parent
|
)
|
}
|
|
const mode: string = this.mode
|
|
// warn invalid mode
|
if (process.env.NODE_ENV !== 'production' &&
|
mode && mode !== 'in-out' && mode !== 'out-in'
|
) {
|
warn(
|
'invalid <transition> mode: ' + mode,
|
this.$parent
|
)
|
}
|
|
const rawChild: VNode = children[0]
|
|
// if this is a component root node and the component's
|
// parent container node also has transition, skip.
|
if (hasParentTransition(this.$vnode)) {
|
return rawChild
|
}
|
|
// apply transition data to child
|
// use getRealChild() to ignore abstract components e.g. keep-alive
|
const child: ?VNode = getRealChild(rawChild)
|
/* istanbul ignore if */
|
if (!child) {
|
return rawChild
|
}
|
|
if (this._leaving) {
|
return placeholder(h, rawChild)
|
}
|
|
// ensure a key that is unique to the vnode type and to this transition
|
// component instance. This key will be used to remove pending leaving nodes
|
// during entering.
|
const id: string = `__transition-${this._uid}-`
|
child.key = child.key == null
|
? child.isComment
|
? id + 'comment'
|
: id + child.tag
|
: isPrimitive(child.key)
|
? (String(child.key).indexOf(id) === 0 ? child.key : id + child.key)
|
: child.key
|
|
const data: Object = (child.data || (child.data = {})).transition = extractTransitionData(this)
|
const oldRawChild: VNode = this._vnode
|
const oldChild: VNode = getRealChild(oldRawChild)
|
|
// mark v-show
|
// so that the transition module can hand over the control to the directive
|
if (child.data.directives && child.data.directives.some(isVShowDirective)) {
|
child.data.show = true
|
}
|
|
if (
|
oldChild &&
|
oldChild.data &&
|
!isSameChild(child, oldChild) &&
|
!isAsyncPlaceholder(oldChild) &&
|
// #6687 component root is a comment node
|
!(oldChild.componentInstance && oldChild.componentInstance._vnode.isComment)
|
) {
|
// replace old child transition data with fresh one
|
// important for dynamic transitions!
|
const oldData: Object = oldChild.data.transition = extend({}, data)
|
// handle transition mode
|
if (mode === 'out-in') {
|
// return placeholder node and queue update when leave finishes
|
this._leaving = true
|
mergeVNodeHook(oldData, 'afterLeave', () => {
|
this._leaving = false
|
this.$forceUpdate()
|
})
|
return placeholder(h, rawChild)
|
} else if (mode === 'in-out') {
|
if (isAsyncPlaceholder(child)) {
|
return oldRawChild
|
}
|
let delayedLeave
|
const performLeave = () => { delayedLeave() }
|
mergeVNodeHook(data, 'afterEnter', performLeave)
|
mergeVNodeHook(data, 'enterCancelled', performLeave)
|
mergeVNodeHook(oldData, 'delayLeave', leave => { delayedLeave = leave })
|
}
|
}
|
|
return rawChild
|
}
|
}
|