diff options
Diffstat (limited to 'node_modules/vue/src/platforms/web/runtime')
18 files changed, 1788 insertions, 0 deletions
diff --git a/node_modules/vue/src/platforms/web/runtime/class-util.js b/node_modules/vue/src/platforms/web/runtime/class-util.js new file mode 100644 index 00000000..709526c2 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/class-util.js @@ -0,0 +1,61 @@ +/* @flow */ + +/** + * Add class with compatibility for SVG since classList is not supported on + * SVG elements in IE + */ +export function addClass (el: HTMLElement, cls: ?string) { + /* istanbul ignore if */ + if (!cls || !(cls = cls.trim())) { + return + } + + /* istanbul ignore else */ + if (el.classList) { + if (cls.indexOf(' ') > -1) { + cls.split(/\s+/).forEach(c => el.classList.add(c)) + } else { + el.classList.add(cls) + } + } else { + const cur = ` ${el.getAttribute('class') || ''} ` + if (cur.indexOf(' ' + cls + ' ') < 0) { + el.setAttribute('class', (cur + cls).trim()) + } + } +} + +/** + * Remove class with compatibility for SVG since classList is not supported on + * SVG elements in IE + */ +export function removeClass (el: HTMLElement, cls: ?string) { + /* istanbul ignore if */ + if (!cls || !(cls = cls.trim())) { + return + } + + /* istanbul ignore else */ + if (el.classList) { + if (cls.indexOf(' ') > -1) { + cls.split(/\s+/).forEach(c => el.classList.remove(c)) + } else { + el.classList.remove(cls) + } + if (!el.classList.length) { + el.removeAttribute('class') + } + } else { + let cur = ` ${el.getAttribute('class') || ''} ` + const tar = ' ' + cls + ' ' + while (cur.indexOf(tar) >= 0) { + cur = cur.replace(tar, ' ') + } + cur = cur.trim() + if (cur) { + el.setAttribute('class', cur) + } else { + el.removeAttribute('class') + } + } +} diff --git a/node_modules/vue/src/platforms/web/runtime/components/index.js b/node_modules/vue/src/platforms/web/runtime/components/index.js new file mode 100644 index 00000000..6bfe5780 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/components/index.js @@ -0,0 +1,7 @@ +import Transition from './transition' +import TransitionGroup from './transition-group' + +export default { + Transition, + TransitionGroup +} diff --git a/node_modules/vue/src/platforms/web/runtime/components/transition-group.js b/node_modules/vue/src/platforms/web/runtime/components/transition-group.js new file mode 100644 index 00000000..219ff01c --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/components/transition-group.js @@ -0,0 +1,180 @@ +/* @flow */ + +// 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, +// we force transition-group to update its children into two passes: +// in the first pass, we remove all nodes that need to be removed, +// triggering their leaving transition; in the second pass, we insert/move +// into the final desired state. This way in the second pass removed +// nodes will remain where they should be. + +import { warn, extend } from 'core/util/index' +import { addClass, removeClass } from '../class-util' +import { transitionProps, extractTransitionData } from './transition' + +import { + hasTransition, + getTransitionInfo, + transitionEndEvent, + addTransitionClass, + removeTransitionClass +} from '../transition-util' + +const props = extend({ + tag: String, + moveClass: String +}, transitionProps) + +delete props.mode + +export default { + props, + + render (h: Function) { + const tag: string = this.tag || this.$vnode.data.tag || 'span' + const map: Object = Object.create(null) + const prevChildren: Array<VNode> = this.prevChildren = this.children + const rawChildren: Array<VNode> = this.$slots.default || [] + const children: Array<VNode> = this.children = [] + const transitionData: Object = extractTransitionData(this) + + for (let i = 0; i < rawChildren.length; i++) { + const c: VNode = rawChildren[i] + if (c.tag) { + if (c.key != null && String(c.key).indexOf('__vlist') !== 0) { + children.push(c) + map[c.key] = c + ;(c.data || (c.data = {})).transition = transitionData + } else if (process.env.NODE_ENV !== 'production') { + const opts: ?VNodeComponentOptions = c.componentOptions + const name: string = opts ? (opts.Ctor.options.name || opts.tag || '') : c.tag + warn(`<transition-group> children must be keyed: <${name}>`) + } + } + } + + if (prevChildren) { + const kept: Array<VNode> = [] + const removed: Array<VNode> = [] + for (let i = 0; i < prevChildren.length; i++) { + const c: VNode = prevChildren[i] + c.data.transition = transitionData + c.data.pos = c.elm.getBoundingClientRect() + if (map[c.key]) { + kept.push(c) + } else { + removed.push(c) + } + } + this.kept = h(tag, null, kept) + this.removed = removed + } + + return h(tag, null, children) + }, + + beforeUpdate () { + // force removing pass + this.__patch__( + this._vnode, + this.kept, + false, // hydrating + true // removeOnly (!important, avoids unnecessary moves) + ) + this._vnode = this.kept + }, + + updated () { + const children: Array<VNode> = this.prevChildren + const moveClass: string = this.moveClass || ((this.name || 'v') + '-move') + if (!children.length || !this.hasMove(children[0].elm, moveClass)) { + return + } + + // we divide the work into three loops to avoid mixing DOM reads and writes + // in each iteration - which helps prevent layout thrashing. + children.forEach(callPendingCbs) + children.forEach(recordPosition) + children.forEach(applyTranslation) + + // force reflow to put everything in position + // assign to this to avoid being removed in tree-shaking + // $flow-disable-line + this._reflow = document.body.offsetHeight + + children.forEach((c: VNode) => { + if (c.data.moved) { + var el: any = c.elm + var s: any = el.style + addTransitionClass(el, moveClass) + s.transform = s.WebkitTransform = s.transitionDuration = '' + el.addEventListener(transitionEndEvent, el._moveCb = function cb (e) { + if (!e || /transform$/.test(e.propertyName)) { + el.removeEventListener(transitionEndEvent, cb) + el._moveCb = null + removeTransitionClass(el, moveClass) + } + }) + } + }) + }, + + methods: { + hasMove (el: any, moveClass: string): boolean { + /* istanbul ignore if */ + if (!hasTransition) { + return false + } + /* istanbul ignore if */ + if (this._hasMove) { + return this._hasMove + } + // Detect whether an element with the move class applied has + // CSS transitions. Since the element may be inside an entering + // transition at this very moment, we make a clone of it and remove + // all other transition classes applied to ensure only the move class + // is applied. + const clone: HTMLElement = el.cloneNode() + if (el._transitionClasses) { + el._transitionClasses.forEach((cls: string) => { removeClass(clone, cls) }) + } + addClass(clone, moveClass) + clone.style.display = 'none' + this.$el.appendChild(clone) + const info: Object = getTransitionInfo(clone) + this.$el.removeChild(clone) + return (this._hasMove = info.hasTransform) + } + } +} + +function callPendingCbs (c: VNode) { + /* istanbul ignore if */ + if (c.elm._moveCb) { + c.elm._moveCb() + } + /* istanbul ignore if */ + if (c.elm._enterCb) { + c.elm._enterCb() + } +} + +function recordPosition (c: VNode) { + c.data.newPos = c.elm.getBoundingClientRect() +} + +function applyTranslation (c: VNode) { + const oldPos = c.data.pos + const newPos = c.data.newPos + 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' + } +} diff --git a/node_modules/vue/src/platforms/web/runtime/components/transition.js b/node_modules/vue/src/platforms/web/runtime/components/transition.js new file mode 100644 index 00000000..6111d8cb --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/components/transition.js @@ -0,0 +1,194 @@ +/* @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 +} + +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((c: VNode) => c.tag || isAsyncPlaceholder(c)) + /* 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(d => d.name === 'show')) { + 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 + } +} diff --git a/node_modules/vue/src/platforms/web/runtime/directives/index.js b/node_modules/vue/src/platforms/web/runtime/directives/index.js new file mode 100644 index 00000000..b673f11a --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/directives/index.js @@ -0,0 +1,7 @@ +import model from './model' +import show from './show' + +export default { + model, + show +} diff --git a/node_modules/vue/src/platforms/web/runtime/directives/model.js b/node_modules/vue/src/platforms/web/runtime/directives/model.js new file mode 100644 index 00000000..cba8ded0 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/directives/model.js @@ -0,0 +1,147 @@ +/** + * Not type checking this file because flow doesn't like attaching + * properties to Elements. + */ + +import { isTextInputType } from 'web/util/element' +import { looseEqual, looseIndexOf } from 'shared/util' +import { mergeVNodeHook } from 'core/vdom/helpers/index' +import { warn, isIE9, isIE, isEdge } from 'core/util/index' + +/* istanbul ignore if */ +if (isIE9) { + // http://www.matts411.com/post/internet-explorer-9-oninput/ + document.addEventListener('selectionchange', () => { + const el = document.activeElement + if (el && el.vmodel) { + trigger(el, 'input') + } + }) +} + +const directive = { + inserted (el, binding, vnode, oldVnode) { + if (vnode.tag === 'select') { + // #6903 + if (oldVnode.elm && !oldVnode.elm._vOptions) { + mergeVNodeHook(vnode, 'postpatch', () => { + directive.componentUpdated(el, binding, vnode) + }) + } else { + setSelected(el, binding, vnode.context) + } + el._vOptions = [].map.call(el.options, getValue) + } else if (vnode.tag === 'textarea' || isTextInputType(el.type)) { + el._vModifiers = binding.modifiers + if (!binding.modifiers.lazy) { + el.addEventListener('compositionstart', onCompositionStart) + el.addEventListener('compositionend', onCompositionEnd) + // Safari < 10.2 & UIWebView doesn't fire compositionend when + // switching focus before confirming composition choice + // this also fixes the issue where some browsers e.g. iOS Chrome + // fires "change" instead of "input" on autocomplete. + el.addEventListener('change', onCompositionEnd) + /* istanbul ignore if */ + if (isIE9) { + el.vmodel = true + } + } + } + }, + + componentUpdated (el, binding, vnode) { + if (vnode.tag === 'select') { + setSelected(el, binding, vnode.context) + // in case the options rendered by v-for have changed, + // it's possible that the value is out-of-sync with the rendered options. + // detect such cases and filter out values that no longer has a matching + // option in the DOM. + const prevOptions = el._vOptions + const curOptions = el._vOptions = [].map.call(el.options, getValue) + if (curOptions.some((o, i) => !looseEqual(o, prevOptions[i]))) { + // trigger change event if + // no matching option found for at least one value + const needReset = el.multiple + ? binding.value.some(v => hasNoMatchingOption(v, curOptions)) + : binding.value !== binding.oldValue && hasNoMatchingOption(binding.value, curOptions) + if (needReset) { + trigger(el, 'change') + } + } + } + } +} + +function setSelected (el, binding, vm) { + actuallySetSelected(el, binding, vm) + /* istanbul ignore if */ + if (isIE || isEdge) { + setTimeout(() => { + actuallySetSelected(el, binding, vm) + }, 0) + } +} + +function actuallySetSelected (el, binding, vm) { + const value = binding.value + const isMultiple = el.multiple + if (isMultiple && !Array.isArray(value)) { + process.env.NODE_ENV !== 'production' && warn( + `<select multiple v-model="${binding.expression}"> ` + + `expects an Array value for its binding, but got ${ + Object.prototype.toString.call(value).slice(8, -1) + }`, + vm + ) + return + } + let selected, option + for (let i = 0, l = el.options.length; i < l; i++) { + option = el.options[i] + if (isMultiple) { + selected = looseIndexOf(value, getValue(option)) > -1 + if (option.selected !== selected) { + option.selected = selected + } + } else { + if (looseEqual(getValue(option), value)) { + if (el.selectedIndex !== i) { + el.selectedIndex = i + } + return + } + } + } + if (!isMultiple) { + el.selectedIndex = -1 + } +} + +function hasNoMatchingOption (value, options) { + return options.every(o => !looseEqual(o, value)) +} + +function getValue (option) { + return '_value' in option + ? option._value + : option.value +} + +function onCompositionStart (e) { + e.target.composing = true +} + +function onCompositionEnd (e) { + // prevent triggering an input event for no reason + if (!e.target.composing) return + e.target.composing = false + trigger(e.target, 'input') +} + +function trigger (el, type) { + const e = document.createEvent('HTMLEvents') + e.initEvent(type, true, true) + el.dispatchEvent(e) +} + +export default directive diff --git a/node_modules/vue/src/platforms/web/runtime/directives/show.js b/node_modules/vue/src/platforms/web/runtime/directives/show.js new file mode 100644 index 00000000..17fb5dec --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/directives/show.js @@ -0,0 +1,60 @@ +/* @flow */ + +import { enter, leave } from '../modules/transition' + +// recursively search for possible transition defined inside the component root +function locateNode (vnode: VNode): VNodeWithData { + return vnode.componentInstance && (!vnode.data || !vnode.data.transition) + ? locateNode(vnode.componentInstance._vnode) + : vnode +} + +export default { + bind (el: any, { value }: VNodeDirective, vnode: VNodeWithData) { + vnode = locateNode(vnode) + const transition = vnode.data && vnode.data.transition + const originalDisplay = el.__vOriginalDisplay = + el.style.display === 'none' ? '' : el.style.display + if (value && transition) { + vnode.data.show = true + enter(vnode, () => { + el.style.display = originalDisplay + }) + } else { + el.style.display = value ? originalDisplay : 'none' + } + }, + + update (el: any, { value, oldValue }: VNodeDirective, vnode: VNodeWithData) { + /* istanbul ignore if */ + if (!value === !oldValue) return + vnode = locateNode(vnode) + const transition = vnode.data && vnode.data.transition + if (transition) { + vnode.data.show = true + if (value) { + enter(vnode, () => { + el.style.display = el.__vOriginalDisplay + }) + } else { + leave(vnode, () => { + el.style.display = 'none' + }) + } + } else { + el.style.display = value ? el.__vOriginalDisplay : 'none' + } + }, + + unbind ( + el: any, + binding: VNodeDirective, + vnode: VNodeWithData, + oldVnode: VNodeWithData, + isDestroy: boolean + ) { + if (!isDestroy) { + el.style.display = el.__vOriginalDisplay + } + } +} diff --git a/node_modules/vue/src/platforms/web/runtime/index.js b/node_modules/vue/src/platforms/web/runtime/index.js new file mode 100644 index 00000000..1283f521 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/index.js @@ -0,0 +1,77 @@ +/* @flow */ + +import Vue from 'core/index' +import config from 'core/config' +import { extend, noop } from 'shared/util' +import { mountComponent } from 'core/instance/lifecycle' +import { devtools, inBrowser, isChrome } from 'core/util/index' + +import { + query, + mustUseProp, + isReservedTag, + isReservedAttr, + getTagNamespace, + isUnknownElement +} from 'web/util/index' + +import { patch } from './patch' +import platformDirectives from './directives/index' +import platformComponents from './components/index' + +// install platform specific utils +Vue.config.mustUseProp = mustUseProp +Vue.config.isReservedTag = isReservedTag +Vue.config.isReservedAttr = isReservedAttr +Vue.config.getTagNamespace = getTagNamespace +Vue.config.isUnknownElement = isUnknownElement + +// install platform runtime directives & components +extend(Vue.options.directives, platformDirectives) +extend(Vue.options.components, platformComponents) + +// install platform patch function +Vue.prototype.__patch__ = inBrowser ? patch : noop + +// public mount method +Vue.prototype.$mount = function ( + el?: string | Element, + hydrating?: boolean +): Component { + el = el && inBrowser ? query(el) : undefined + return mountComponent(this, el, hydrating) +} + +// devtools global hook +/* istanbul ignore next */ +if (inBrowser) { + setTimeout(() => { + if (config.devtools) { + if (devtools) { + devtools.emit('init', Vue) + } else if ( + process.env.NODE_ENV !== 'production' && + process.env.NODE_ENV !== 'test' && + isChrome + ) { + console[console.info ? 'info' : 'log']( + 'Download the Vue Devtools extension for a better development experience:\n' + + 'https://github.com/vuejs/vue-devtools' + ) + } + } + if (process.env.NODE_ENV !== 'production' && + process.env.NODE_ENV !== 'test' && + config.productionTip !== false && + typeof console !== 'undefined' + ) { + console[console.info ? 'info' : 'log']( + `You are running Vue in development mode.\n` + + `Make sure to turn on production mode when deploying for production.\n` + + `See more tips at https://vuejs.org/guide/deployment.html` + ) + } + }, 0) +} + +export default Vue diff --git a/node_modules/vue/src/platforms/web/runtime/modules/attrs.js b/node_modules/vue/src/platforms/web/runtime/modules/attrs.js new file mode 100644 index 00000000..78ef28a1 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/modules/attrs.js @@ -0,0 +1,118 @@ +/* @flow */ + +import { isIE, isIE9, isEdge } from 'core/util/env' + +import { + extend, + isDef, + isUndef +} from 'shared/util' + +import { + isXlink, + xlinkNS, + getXlinkProp, + isBooleanAttr, + isEnumeratedAttr, + isFalsyAttrValue +} from 'web/util/index' + +function updateAttrs (oldVnode: VNodeWithData, vnode: VNodeWithData) { + const opts = vnode.componentOptions + if (isDef(opts) && opts.Ctor.options.inheritAttrs === false) { + return + } + if (isUndef(oldVnode.data.attrs) && isUndef(vnode.data.attrs)) { + return + } + let key, cur, old + const elm = vnode.elm + const oldAttrs = oldVnode.data.attrs || {} + let attrs: any = vnode.data.attrs || {} + // clone observed objects, as the user probably wants to mutate it + if (isDef(attrs.__ob__)) { + attrs = vnode.data.attrs = extend({}, attrs) + } + + for (key in attrs) { + cur = attrs[key] + old = oldAttrs[key] + if (old !== cur) { + setAttr(elm, key, cur) + } + } + // #4391: in IE9, setting type can reset value for input[type=radio] + // #6666: IE/Edge forces progress value down to 1 before setting a max + /* istanbul ignore if */ + if ((isIE || isEdge) && attrs.value !== oldAttrs.value) { + setAttr(elm, 'value', attrs.value) + } + for (key in oldAttrs) { + if (isUndef(attrs[key])) { + if (isXlink(key)) { + elm.removeAttributeNS(xlinkNS, getXlinkProp(key)) + } else if (!isEnumeratedAttr(key)) { + elm.removeAttribute(key) + } + } + } +} + +function setAttr (el: Element, key: string, value: any) { + if (el.tagName.indexOf('-') > -1) { + baseSetAttr(el, key, value) + } else if (isBooleanAttr(key)) { + // set attribute for blank value + // e.g. <option disabled>Select one</option> + if (isFalsyAttrValue(value)) { + el.removeAttribute(key) + } else { + // technically allowfullscreen is a boolean attribute for <iframe>, + // but Flash expects a value of "true" when used on <embed> tag + value = key === 'allowfullscreen' && el.tagName === 'EMBED' + ? 'true' + : key + el.setAttribute(key, value) + } + } else if (isEnumeratedAttr(key)) { + el.setAttribute(key, isFalsyAttrValue(value) || value === 'false' ? 'false' : 'true') + } else if (isXlink(key)) { + if (isFalsyAttrValue(value)) { + el.removeAttributeNS(xlinkNS, getXlinkProp(key)) + } else { + el.setAttributeNS(xlinkNS, key, value) + } + } else { + baseSetAttr(el, key, value) + } +} + +function baseSetAttr (el, key, value) { + if (isFalsyAttrValue(value)) { + el.removeAttribute(key) + } else { + // #7138: IE10 & 11 fires input event when setting placeholder on + // <textarea>... block the first input event and remove the blocker + // immediately. + /* istanbul ignore if */ + if ( + isIE && !isIE9 && + el.tagName === 'TEXTAREA' && + key === 'placeholder' && !el.__ieph + ) { + const blocker = e => { + e.stopImmediatePropagation() + el.removeEventListener('input', blocker) + } + el.addEventListener('input', blocker) + // $flow-disable-line + el.__ieph = true /* IE placeholder patched */ + } + el.setAttribute(key, value) + } +} + +export default { + create: updateAttrs, + update: updateAttrs +} diff --git a/node_modules/vue/src/platforms/web/runtime/modules/class.js b/node_modules/vue/src/platforms/web/runtime/modules/class.js new file mode 100644 index 00000000..29fd2c11 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/modules/class.js @@ -0,0 +1,48 @@ +/* @flow */ + +import { + isDef, + isUndef +} from 'shared/util' + +import { + concat, + stringifyClass, + genClassForVnode +} from 'web/util/index' + +function updateClass (oldVnode: any, vnode: any) { + const el = vnode.elm + const data: VNodeData = vnode.data + const oldData: VNodeData = oldVnode.data + if ( + isUndef(data.staticClass) && + isUndef(data.class) && ( + isUndef(oldData) || ( + isUndef(oldData.staticClass) && + isUndef(oldData.class) + ) + ) + ) { + return + } + + let cls = genClassForVnode(vnode) + + // handle transition classes + const transitionClass = el._transitionClasses + if (isDef(transitionClass)) { + cls = concat(cls, stringifyClass(transitionClass)) + } + + // set the class + if (cls !== el._prevClass) { + el.setAttribute('class', cls) + el._prevClass = cls + } +} + +export default { + create: updateClass, + update: updateClass +} diff --git a/node_modules/vue/src/platforms/web/runtime/modules/dom-props.js b/node_modules/vue/src/platforms/web/runtime/modules/dom-props.js new file mode 100644 index 00000000..f3888aed --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/modules/dom-props.js @@ -0,0 +1,95 @@ +/* @flow */ + +import { isDef, isUndef, extend, toNumber } from 'shared/util' + +function updateDOMProps (oldVnode: VNodeWithData, vnode: VNodeWithData) { + if (isUndef(oldVnode.data.domProps) && isUndef(vnode.data.domProps)) { + return + } + let key, cur + const elm: any = vnode.elm + const oldProps = oldVnode.data.domProps || {} + let props = vnode.data.domProps || {} + // clone observed objects, as the user probably wants to mutate it + if (isDef(props.__ob__)) { + props = vnode.data.domProps = extend({}, props) + } + + for (key in oldProps) { + if (isUndef(props[key])) { + elm[key] = '' + } + } + for (key in props) { + cur = props[key] + // ignore children if the node has textContent or innerHTML, + // as these will throw away existing DOM nodes and cause removal errors + // on subsequent patches (#3360) + if (key === 'textContent' || key === 'innerHTML') { + if (vnode.children) vnode.children.length = 0 + if (cur === oldProps[key]) continue + // #6601 work around Chrome version <= 55 bug where single textNode + // replaced by innerHTML/textContent retains its parentNode property + if (elm.childNodes.length === 1) { + elm.removeChild(elm.childNodes[0]) + } + } + + if (key === 'value') { + // store value as _value as well since + // non-string values will be stringified + elm._value = cur + // avoid resetting cursor position when value is the same + const strCur = isUndef(cur) ? '' : String(cur) + if (shouldUpdateValue(elm, strCur)) { + elm.value = strCur + } + } else { + elm[key] = cur + } + } +} + +// check platforms/web/util/attrs.js acceptValue +type acceptValueElm = HTMLInputElement | HTMLSelectElement | HTMLOptionElement; + +function shouldUpdateValue (elm: acceptValueElm, checkVal: string): boolean { + return (!elm.composing && ( + elm.tagName === 'OPTION' || + isNotInFocusAndDirty(elm, checkVal) || + isDirtyWithModifiers(elm, checkVal) + )) +} + +function isNotInFocusAndDirty (elm: acceptValueElm, checkVal: string): boolean { + // return true when textbox (.number and .trim) loses focus and its value is + // not equal to the updated value + let notInFocus = true + // #6157 + // work around IE bug when accessing document.activeElement in an iframe + try { notInFocus = document.activeElement !== elm } catch (e) {} + return notInFocus && elm.value !== checkVal +} + +function isDirtyWithModifiers (elm: any, newVal: string): boolean { + const value = elm.value + const modifiers = elm._vModifiers // injected by v-model runtime + if (isDef(modifiers)) { + if (modifiers.lazy) { + // inputs with lazy should only be updated when not in focus + return false + } + if (modifiers.number) { + return toNumber(value) !== toNumber(newVal) + } + if (modifiers.trim) { + return value.trim() !== newVal.trim() + } + } + return value !== newVal +} + +export default { + create: updateDOMProps, + update: updateDOMProps +} diff --git a/node_modules/vue/src/platforms/web/runtime/modules/events.js b/node_modules/vue/src/platforms/web/runtime/modules/events.js new file mode 100644 index 00000000..1e71b667 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/modules/events.js @@ -0,0 +1,87 @@ +/* @flow */ + +import { isDef, isUndef } from 'shared/util' +import { updateListeners } from 'core/vdom/helpers/index' +import { withMacroTask, isIE, supportsPassive } from 'core/util/index' +import { RANGE_TOKEN, CHECKBOX_RADIO_TOKEN } from 'web/compiler/directives/model' + +// normalize v-model event tokens that can only be determined at runtime. +// it's important to place the event as the first in the array because +// the whole point is ensuring the v-model callback gets called before +// user-attached handlers. +function normalizeEvents (on) { + /* istanbul ignore if */ + if (isDef(on[RANGE_TOKEN])) { + // IE input[type=range] only supports `change` event + const event = isIE ? 'change' : 'input' + on[event] = [].concat(on[RANGE_TOKEN], on[event] || []) + delete on[RANGE_TOKEN] + } + // This was originally intended to fix #4521 but no longer necessary + // after 2.5. Keeping it for backwards compat with generated code from < 2.4 + /* istanbul ignore if */ + if (isDef(on[CHECKBOX_RADIO_TOKEN])) { + on.change = [].concat(on[CHECKBOX_RADIO_TOKEN], on.change || []) + delete on[CHECKBOX_RADIO_TOKEN] + } +} + +let target: any + +function createOnceHandler (handler, event, capture) { + const _target = target // save current target element in closure + return function onceHandler () { + const res = handler.apply(null, arguments) + if (res !== null) { + remove(event, onceHandler, capture, _target) + } + } +} + +function add ( + event: string, + handler: Function, + once: boolean, + capture: boolean, + passive: boolean +) { + handler = withMacroTask(handler) + if (once) handler = createOnceHandler(handler, event, capture) + target.addEventListener( + event, + handler, + supportsPassive + ? { capture, passive } + : capture + ) +} + +function remove ( + event: string, + handler: Function, + capture: boolean, + _target?: HTMLElement +) { + (_target || target).removeEventListener( + event, + handler._withTask || handler, + capture + ) +} + +function updateDOMListeners (oldVnode: VNodeWithData, vnode: VNodeWithData) { + if (isUndef(oldVnode.data.on) && isUndef(vnode.data.on)) { + return + } + const on = vnode.data.on || {} + const oldOn = oldVnode.data.on || {} + target = vnode.elm + normalizeEvents(on) + updateListeners(on, oldOn, add, remove, vnode.context) + target = undefined +} + +export default { + create: updateDOMListeners, + update: updateDOMListeners +} diff --git a/node_modules/vue/src/platforms/web/runtime/modules/index.js b/node_modules/vue/src/platforms/web/runtime/modules/index.js new file mode 100644 index 00000000..9f6ad8f9 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/modules/index.js @@ -0,0 +1,15 @@ +import attrs from './attrs' +import klass from './class' +import events from './events' +import domProps from './dom-props' +import style from './style' +import transition from './transition' + +export default [ + attrs, + klass, + events, + domProps, + style, + transition +] diff --git a/node_modules/vue/src/platforms/web/runtime/modules/style.js b/node_modules/vue/src/platforms/web/runtime/modules/style.js new file mode 100644 index 00000000..29e1c3c1 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/modules/style.js @@ -0,0 +1,93 @@ +/* @flow */ + +import { getStyle, normalizeStyleBinding } from 'web/util/style' +import { cached, camelize, extend, isDef, isUndef } from 'shared/util' + +const cssVarRE = /^--/ +const importantRE = /\s*!important$/ +const setProp = (el, name, val) => { + /* istanbul ignore if */ + if (cssVarRE.test(name)) { + el.style.setProperty(name, val) + } else if (importantRE.test(val)) { + el.style.setProperty(name, val.replace(importantRE, ''), 'important') + } else { + const normalizedName = normalize(name) + if (Array.isArray(val)) { + // Support values array created by autoprefixer, e.g. + // {display: ["-webkit-box", "-ms-flexbox", "flex"]} + // Set them one by one, and the browser will only set those it can recognize + for (let i = 0, len = val.length; i < len; i++) { + el.style[normalizedName] = val[i] + } + } else { + el.style[normalizedName] = val + } + } +} + +const vendorNames = ['Webkit', 'Moz', 'ms'] + +let emptyStyle +const normalize = cached(function (prop) { + emptyStyle = emptyStyle || document.createElement('div').style + prop = camelize(prop) + if (prop !== 'filter' && (prop in emptyStyle)) { + return prop + } + const capName = prop.charAt(0).toUpperCase() + prop.slice(1) + for (let i = 0; i < vendorNames.length; i++) { + const name = vendorNames[i] + capName + if (name in emptyStyle) { + return name + } + } +}) + +function updateStyle (oldVnode: VNodeWithData, vnode: VNodeWithData) { + const data = vnode.data + const oldData = oldVnode.data + + if (isUndef(data.staticStyle) && isUndef(data.style) && + isUndef(oldData.staticStyle) && isUndef(oldData.style) + ) { + return + } + + let cur, name + const el: any = vnode.elm + const oldStaticStyle: any = oldData.staticStyle + const oldStyleBinding: any = oldData.normalizedStyle || oldData.style || {} + + // if static style exists, stylebinding already merged into it when doing normalizeStyleData + const oldStyle = oldStaticStyle || oldStyleBinding + + const style = normalizeStyleBinding(vnode.data.style) || {} + + // store normalized style under a different key for next diff + // make sure to clone it if it's reactive, since the user likely wants + // to mutate it. + vnode.data.normalizedStyle = isDef(style.__ob__) + ? extend({}, style) + : style + + const newStyle = getStyle(vnode, true) + + for (name in oldStyle) { + if (isUndef(newStyle[name])) { + setProp(el, name, '') + } + } + for (name in newStyle) { + cur = newStyle[name] + if (cur !== oldStyle[name]) { + // ie9 setting to null has no effect, must use empty string + setProp(el, name, cur == null ? '' : cur) + } + } +} + +export default { + create: updateStyle, + update: updateStyle +} diff --git a/node_modules/vue/src/platforms/web/runtime/modules/transition.js b/node_modules/vue/src/platforms/web/runtime/modules/transition.js new file mode 100644 index 00000000..0796b5da --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/modules/transition.js @@ -0,0 +1,343 @@ +/* @flow */ + +import { inBrowser, isIE9, warn } from 'core/util/index' +import { mergeVNodeHook } from 'core/vdom/helpers/index' +import { activeInstance } from 'core/instance/lifecycle' + +import { + once, + isDef, + isUndef, + isObject, + toNumber +} from 'shared/util' + +import { + nextFrame, + resolveTransition, + whenTransitionEnds, + addTransitionClass, + removeTransitionClass +} from '../transition-util' + +export function enter (vnode: VNodeWithData, toggleDisplay: ?() => void) { + const el: any = vnode.elm + + // call leave callback now + if (isDef(el._leaveCb)) { + el._leaveCb.cancelled = true + el._leaveCb() + } + + const data = resolveTransition(vnode.data.transition) + if (isUndef(data)) { + return + } + + /* istanbul ignore if */ + if (isDef(el._enterCb) || el.nodeType !== 1) { + return + } + + const { + css, + type, + enterClass, + enterToClass, + enterActiveClass, + appearClass, + appearToClass, + appearActiveClass, + beforeEnter, + enter, + afterEnter, + enterCancelled, + beforeAppear, + appear, + afterAppear, + appearCancelled, + duration + } = data + + // activeInstance will always be the <transition> component managing this + // transition. One edge case to check is when the <transition> is placed + // as the root node of a child component. In that case we need to check + // <transition>'s parent for appear check. + let context = activeInstance + let transitionNode = activeInstance.$vnode + while (transitionNode && transitionNode.parent) { + transitionNode = transitionNode.parent + context = transitionNode.context + } + + const isAppear = !context._isMounted || !vnode.isRootInsert + + if (isAppear && !appear && appear !== '') { + return + } + + const startClass = isAppear && appearClass + ? appearClass + : enterClass + const activeClass = isAppear && appearActiveClass + ? appearActiveClass + : enterActiveClass + const toClass = isAppear && appearToClass + ? appearToClass + : enterToClass + + const beforeEnterHook = isAppear + ? (beforeAppear || beforeEnter) + : beforeEnter + const enterHook = isAppear + ? (typeof appear === 'function' ? appear : enter) + : enter + const afterEnterHook = isAppear + ? (afterAppear || afterEnter) + : afterEnter + const enterCancelledHook = isAppear + ? (appearCancelled || enterCancelled) + : enterCancelled + + const explicitEnterDuration: any = toNumber( + isObject(duration) + ? duration.enter + : duration + ) + + if (process.env.NODE_ENV !== 'production' && explicitEnterDuration != null) { + checkDuration(explicitEnterDuration, 'enter', vnode) + } + + const expectsCSS = css !== false && !isIE9 + const userWantsControl = getHookArgumentsLength(enterHook) + + const cb = el._enterCb = once(() => { + if (expectsCSS) { + removeTransitionClass(el, toClass) + removeTransitionClass(el, activeClass) + } + if (cb.cancelled) { + if (expectsCSS) { + removeTransitionClass(el, startClass) + } + enterCancelledHook && enterCancelledHook(el) + } else { + afterEnterHook && afterEnterHook(el) + } + el._enterCb = null + }) + + if (!vnode.data.show) { + // remove pending leave element on enter by injecting an insert hook + mergeVNodeHook(vnode, 'insert', () => { + const parent = el.parentNode + const pendingNode = parent && parent._pending && parent._pending[vnode.key] + if (pendingNode && + pendingNode.tag === vnode.tag && + pendingNode.elm._leaveCb + ) { + pendingNode.elm._leaveCb() + } + enterHook && enterHook(el, cb) + }) + } + + // start enter transition + beforeEnterHook && beforeEnterHook(el) + if (expectsCSS) { + addTransitionClass(el, startClass) + addTransitionClass(el, activeClass) + nextFrame(() => { + removeTransitionClass(el, startClass) + if (!cb.cancelled) { + addTransitionClass(el, toClass) + if (!userWantsControl) { + if (isValidDuration(explicitEnterDuration)) { + setTimeout(cb, explicitEnterDuration) + } else { + whenTransitionEnds(el, type, cb) + } + } + } + }) + } + + if (vnode.data.show) { + toggleDisplay && toggleDisplay() + enterHook && enterHook(el, cb) + } + + if (!expectsCSS && !userWantsControl) { + cb() + } +} + +export function leave (vnode: VNodeWithData, rm: Function) { + const el: any = vnode.elm + + // call enter callback now + if (isDef(el._enterCb)) { + el._enterCb.cancelled = true + el._enterCb() + } + + const data = resolveTransition(vnode.data.transition) + if (isUndef(data) || el.nodeType !== 1) { + return rm() + } + + /* istanbul ignore if */ + if (isDef(el._leaveCb)) { + return + } + + const { + css, + type, + leaveClass, + leaveToClass, + leaveActiveClass, + beforeLeave, + leave, + afterLeave, + leaveCancelled, + delayLeave, + duration + } = data + + const expectsCSS = css !== false && !isIE9 + const userWantsControl = getHookArgumentsLength(leave) + + const explicitLeaveDuration: any = toNumber( + isObject(duration) + ? duration.leave + : duration + ) + + if (process.env.NODE_ENV !== 'production' && isDef(explicitLeaveDuration)) { + checkDuration(explicitLeaveDuration, 'leave', vnode) + } + + const cb = el._leaveCb = once(() => { + if (el.parentNode && el.parentNode._pending) { + el.parentNode._pending[vnode.key] = null + } + if (expectsCSS) { + removeTransitionClass(el, leaveToClass) + removeTransitionClass(el, leaveActiveClass) + } + if (cb.cancelled) { + if (expectsCSS) { + removeTransitionClass(el, leaveClass) + } + leaveCancelled && leaveCancelled(el) + } else { + rm() + afterLeave && afterLeave(el) + } + el._leaveCb = null + }) + + if (delayLeave) { + delayLeave(performLeave) + } else { + performLeave() + } + + function performLeave () { + // the delayed leave may have already been cancelled + if (cb.cancelled) { + return + } + // record leaving element + if (!vnode.data.show) { + (el.parentNode._pending || (el.parentNode._pending = {}))[(vnode.key: any)] = vnode + } + beforeLeave && beforeLeave(el) + if (expectsCSS) { + addTransitionClass(el, leaveClass) + addTransitionClass(el, leaveActiveClass) + nextFrame(() => { + removeTransitionClass(el, leaveClass) + if (!cb.cancelled) { + addTransitionClass(el, leaveToClass) + if (!userWantsControl) { + if (isValidDuration(explicitLeaveDuration)) { + setTimeout(cb, explicitLeaveDuration) + } else { + whenTransitionEnds(el, type, cb) + } + } + } + }) + } + leave && leave(el, cb) + if (!expectsCSS && !userWantsControl) { + cb() + } + } +} + +// only used in dev mode +function checkDuration (val, name, vnode) { + if (typeof val !== 'number') { + warn( + `<transition> explicit ${name} duration is not a valid number - ` + + `got ${JSON.stringify(val)}.`, + vnode.context + ) + } else if (isNaN(val)) { + warn( + `<transition> explicit ${name} duration is NaN - ` + + 'the duration expression might be incorrect.', + vnode.context + ) + } +} + +function isValidDuration (val) { + return typeof val === 'number' && !isNaN(val) +} + +/** + * Normalize a transition hook's argument length. The hook may be: + * - a merged hook (invoker) with the original in .fns + * - a wrapped component method (check ._length) + * - a plain function (.length) + */ +function getHookArgumentsLength (fn: Function): boolean { + if (isUndef(fn)) { + return false + } + const invokerFns = fn.fns + if (isDef(invokerFns)) { + // invoker + return getHookArgumentsLength( + Array.isArray(invokerFns) + ? invokerFns[0] + : invokerFns + ) + } else { + return (fn._length || fn.length) > 1 + } +} + +function _enter (_: any, vnode: VNodeWithData) { + if (vnode.data.show !== true) { + enter(vnode) + } +} + +export default inBrowser ? { + create: _enter, + activate: _enter, + remove (vnode: VNode, rm: Function) { + /* istanbul ignore else */ + if (vnode.data.show !== true) { + leave(vnode, rm) + } else { + rm() + } + } +} : {} diff --git a/node_modules/vue/src/platforms/web/runtime/node-ops.js b/node_modules/vue/src/platforms/web/runtime/node-ops.js new file mode 100644 index 00000000..3a2978a7 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/node-ops.js @@ -0,0 +1,59 @@ +/* @flow */ + +import { namespaceMap } from 'web/util/index' + +export function createElement (tagName: string, vnode: VNode): Element { + const elm = document.createElement(tagName) + if (tagName !== 'select') { + return elm + } + // false or null will remove the attribute but undefined will not + if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) { + elm.setAttribute('multiple', 'multiple') + } + return elm +} + +export function createElementNS (namespace: string, tagName: string): Element { + return document.createElementNS(namespaceMap[namespace], tagName) +} + +export function createTextNode (text: string): Text { + return document.createTextNode(text) +} + +export function createComment (text: string): Comment { + return document.createComment(text) +} + +export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) { + parentNode.insertBefore(newNode, referenceNode) +} + +export function removeChild (node: Node, child: Node) { + node.removeChild(child) +} + +export function appendChild (node: Node, child: Node) { + node.appendChild(child) +} + +export function parentNode (node: Node): ?Node { + return node.parentNode +} + +export function nextSibling (node: Node): ?Node { + return node.nextSibling +} + +export function tagName (node: Element): string { + return node.tagName +} + +export function setTextContent (node: Node, text: string) { + node.textContent = text +} + +export function setStyleScope (node: Element, scopeId: string) { + node.setAttribute(scopeId, '') +} diff --git a/node_modules/vue/src/platforms/web/runtime/patch.js b/node_modules/vue/src/platforms/web/runtime/patch.js new file mode 100644 index 00000000..d982afe9 --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/patch.js @@ -0,0 +1,12 @@ +/* @flow */ + +import * as nodeOps from 'web/runtime/node-ops' +import { createPatchFunction } from 'core/vdom/patch' +import baseModules from 'core/vdom/modules/index' +import platformModules from 'web/runtime/modules/index' + +// the directive module should be applied last, after all +// built-in modules have been applied. +const modules = platformModules.concat(baseModules) + +export const patch: Function = createPatchFunction({ nodeOps, modules }) diff --git a/node_modules/vue/src/platforms/web/runtime/transition-util.js b/node_modules/vue/src/platforms/web/runtime/transition-util.js new file mode 100644 index 00000000..47668f0c --- /dev/null +++ b/node_modules/vue/src/platforms/web/runtime/transition-util.js @@ -0,0 +1,185 @@ +/* @flow */ + +import { inBrowser, isIE9 } from 'core/util/index' +import { addClass, removeClass } from './class-util' +import { remove, extend, cached } from 'shared/util' + +export function resolveTransition (def?: string | Object): ?Object { + if (!def) { + return + } + /* istanbul ignore else */ + if (typeof def === 'object') { + const res = {} + if (def.css !== false) { + extend(res, autoCssTransition(def.name || 'v')) + } + extend(res, def) + return res + } else if (typeof def === 'string') { + return autoCssTransition(def) + } +} + +const autoCssTransition: (name: string) => Object = cached(name => { + return { + enterClass: `${name}-enter`, + enterToClass: `${name}-enter-to`, + enterActiveClass: `${name}-enter-active`, + leaveClass: `${name}-leave`, + leaveToClass: `${name}-leave-to`, + leaveActiveClass: `${name}-leave-active` + } +}) + +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' + } +} + +// binding to window is necessary to make hot reload work in IE in strict mode +const raf = inBrowser + ? window.requestAnimationFrame + ? window.requestAnimationFrame.bind(window) + : setTimeout + : /* istanbul ignore next */ fn => fn() + +export function nextFrame (fn: Function) { + raf(() => { + raf(fn) + }) +} + +export function addTransitionClass (el: any, cls: string) { + const transitionClasses = el._transitionClasses || (el._transitionClasses = []) + if (transitionClasses.indexOf(cls) < 0) { + 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, + expectedType: ?string, + cb: Function +) { + const { type, timeout, propCount } = getTransitionInfo(el, expectedType) + if (!type) return cb() + const event: string = type === TRANSITION ? transitionEndEvent : animationEndEvent + let ended = 0 + const end = () => { + el.removeEventListener(event, onEnd) + cb() + } + const onEnd = e => { + if (e.target === el) { + 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, expectedType?: ?string): { + type: ?string; + propCount: number; + timeout: number; + hasTransform: boolean; +} { + const styles: any = window.getComputedStyle(el) + const transitionDelays: Array<string> = styles[transitionProp + 'Delay'].split(', ') + const transitionDurations: Array<string> = styles[transitionProp + 'Duration'].split(', ') + const transitionTimeout: number = getTimeout(transitionDelays, transitionDurations) + const animationDelays: Array<string> = styles[animationProp + 'Delay'].split(', ') + const animationDurations: Array<string> = styles[animationProp + 'Duration'].split(', ') + const animationTimeout: number = getTimeout(animationDelays, animationDurations) + + let type: ?string + let timeout = 0 + let propCount = 0 + /* istanbul ignore if */ + if (expectedType === TRANSITION) { + if (transitionTimeout > 0) { + type = TRANSITION + timeout = transitionTimeout + propCount = transitionDurations.length + } + } else if (expectedType === ANIMATION) { + if (animationTimeout > 0) { + type = ANIMATION + timeout = animationTimeout + propCount = animationDurations.length + } + } else { + timeout = Math.max(transitionTimeout, animationTimeout) + type = timeout > 0 + ? transitionTimeout > animationTimeout + ? TRANSITION + : ANIMATION + : null + propCount = type + ? type === TRANSITION + ? transitionDurations.length + : animationDurations.length + : 0 + } + const hasTransform: boolean = + type === TRANSITION && + transformRE.test(styles[transitionProp + 'Property']) + return { + type, + timeout, + propCount, + hasTransform + } +} + +function getTimeout (delays: Array<string>, durations: Array<string>): number { + /* istanbul ignore next */ + while (delays.length < durations.length) { + delays = delays.concat(delays) + } + + 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 +} |
