RubyLouvre/avalon

View on GitHub
src/effect/index.js

Summary

Maintainability
B
5 hrs
Test Coverage
import { avalon, window, Cache } from '../seed/core'
import { cssDiff } from '../directives/css'
import {
    css3,
    animation,
    transition,
    animationEndEvent,
    transitionEndEvent
} from './detect'

var effectDir = avalon.directive('effect', {
    priority: 5,
    diff: function (effect) {
        var vdom = this.node
        if (typeof effect === 'string') {
            this.value = effect = {
                is: effect
            }
            avalon.warn('ms-effect的指令值不再支持字符串,必须是一个对象')
        }
        this.value = vdom.effect = effect
        var ok = cssDiff.call(this, effect, this.oldValue)
        var me = this
        if (ok) {
            setTimeout(function () {
                vdom.animating = true
                effectDir.update.call(me, vdom, vdom.effect)
            })
            vdom.animating = false
            return true
        }
        return false
    },

    update: function (vdom, change, opts) {
        var dom = vdom.dom
        if (dom && dom.nodeType === 1) {
            //要求配置对象必须指定is属性,action必须是布尔或enter,leave,move
            var option = change || opts
            var is = option.is

            var globalOption = avalon.effects[is]
            if (!globalOption) {//如果没有定义特效
                avalon.warn(is + ' effect is undefined')
                return
            }
            var finalOption = {}
            var action = actionMaps[option.action]
            if (typeof Effect.prototype[action] !== 'function') {
                avalon.warn('action is undefined')
                return
            }
            //必须预定义特效

            var effect = new avalon.Effect(dom)
            avalon.mix(finalOption, globalOption, option, { action })

            if (finalOption.queue) {
                animationQueue.push(function () {
                    effect[action](finalOption)
                })
                callNextAnimation()
            } else {

                effect[action](finalOption)

            }
            return true
        }
    }
})


let move = 'move'
let leave = 'leave'
let enter = 'enter'
var actionMaps = {
    'true': enter,
    'false': leave,
    enter,
    leave,
    move,
    'undefined': enter
}

var animationQueue = []
export function callNextAnimation() {
    var fn = animationQueue[0]
    if (fn) {
        fn()
    }
}

avalon.effects = {}
avalon.effect = function (name, opts) {
    var definition = avalon.effects[name] = (opts || {})
    if (css3 && definition.css !== false) {
        patchObject(definition, 'enterClass', name + '-enter')
        patchObject(definition, 'enterActiveClass', definition.enterClass + '-active')
        patchObject(definition, 'leaveClass', name + '-leave')
        patchObject(definition, 'leaveActiveClass', definition.leaveClass + '-active')
    }
    return definition
}

function patchObject(obj, name, value) {
    if (!obj[name]) {
        obj[name] = value
    }
}

var Effect = function (dom) {
    this.dom = dom
}

avalon.Effect = Effect

Effect.prototype = {
    enter: createAction('Enter'),
    leave: createAction('Leave'),
    move: createAction('Move')
}


function execHooks(options, name, el) {
    var fns = [].concat(options[name])
    for (var i = 0, fn; fn = fns[i++];) {
        if (typeof fn === 'function') {
            fn(el)
        }
    }
}
var staggerCache = new Cache(128)

function createAction(action) {
    var lower = action.toLowerCase()
    return function (option) {
        var dom = this.dom
        var elem = avalon(dom)
        //处理与ms-for指令相关的stagger
        //========BEGIN=====
        var staggerTime = isFinite(option.stagger) ? option.stagger * 1000 : 0
        if (staggerTime) {
            if (option.staggerKey) {
                var stagger = staggerCache.get(option.staggerKey) ||
                    staggerCache.put(option.staggerKey, {
                        count: 0,
                        items: 0
                    })
                stagger.count++
                stagger.items++
            }
        }
        var staggerIndex = stagger && stagger.count || 0
        //=======END==========
        var stopAnimationID
        var animationDone = function (e) {
            var isOk = e !== false
            if (--dom.__ms_effect_ === 0) {
                avalon.unbind(dom, transitionEndEvent)
                avalon.unbind(dom, animationEndEvent)
            }
            clearTimeout(stopAnimationID)
            var dirWord = isOk ? 'Done' : 'Abort'
            execHooks(option, 'on' + action + dirWord, dom)
            if (stagger) {
                if (--stagger.items === 0) {
                    stagger.count = 0
                }
            }
            if (option.queue) {
                animationQueue.shift()
                callNextAnimation()
            }
        }
        //执行开始前的钩子
        execHooks(option, 'onBefore' + action, dom)

        if (option[lower]) {
            //使用JS方式执行动画
            option[lower](dom, function (ok) {
                animationDone(ok !== false)
            })
        } else if (css3) {
            //使用CSS3方式执行动画
            elem.addClass(option[lower + 'Class'])
            elem.removeClass(getNeedRemoved(option, lower))

            if (!dom.__ms_effect_) {
                //绑定动画结束事件
                elem.bind(transitionEndEvent, animationDone)
                elem.bind(animationEndEvent, animationDone)
                dom.__ms_effect_ = 1
            } else {
                dom.__ms_effect_++
            }
            setTimeout(function () {
                //用xxx-active代替xxx类名的方式 触发CSS3动画
                var time = avalon.root.offsetWidth === NaN
                elem.addClass(option[lower + 'ActiveClass'])
                //计算动画时长
                time = getAnimationTime(dom) 
                if (!time === 0) {
                    //立即结束动画
                    animationDone(false)
                } else if (!staggerTime) {
                    //如果动画超出时长还没有调用结束事件,这可能是元素被移除了
                    //如果强制结束动画
                    stopAnimationID = setTimeout(function () {
                        animationDone(false)
                    }, time + 32)
                }
            }, 17 + staggerTime * staggerIndex)// = 1000/60
        }
    }
}

avalon.applyEffect = function (dom, vdom, opts) {
    var cb = opts.cb
    var curEffect = vdom.effect
    if (curEffect && dom && dom.nodeType === 1) {
        var hook = opts.hook
        var old = curEffect[hook]
        if (cb) {
            if (Array.isArray(old)) {
                old.push(cb)
            } else if (old) {
                curEffect[hook] = [old, cb]
            } else {
                curEffect[hook] = [cb]
            }
        }
        getAction(opts)
        avalon.directives.effect.update(vdom, curEffect, avalon.shadowCopy({}, opts))

    } else if (cb) {
        cb(dom)
    }
}
/**
 * 获取方向
 */
export function getAction(opts) {
    if (!opts.action) {
        return opts.action = opts.hook.replace(/^on/, '').replace(/Done$/, '').toLowerCase()
    }
}
/**
 * 需要移除的类名
 */
function getNeedRemoved(options, name) {
    var name = name === 'leave' ? 'enter' : 'leave'
    return Array(name + 'Class', name + 'ActiveClass').map(function (cls) {
        return options[cls]
    }).join(' ')
}
/**
 * 计算动画长度
 */
var transitionDuration = avalon.cssName('transition-duration')
var animationDuration = avalon.cssName('animation-duration')
var rsecond = /\d+s$/
export function toMillisecond(str) {
    var ratio = rsecond.test(str) ? 1000 : 1
    return parseFloat(str) * ratio
}

export function getAnimationTime(dom) {
    var computedStyles = window.getComputedStyle(dom,null)
    var tranDuration = computedStyles[transitionDuration]
    var animDuration = computedStyles[animationDuration]
   return toMillisecond(tranDuration) || toMillisecond(animDuration)
}
/**
 * 
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <script src="dist/avalon.js"></script>
        <script>
            avalon.effect('animate')
            var vm = avalon.define({
                $id: 'ani',
                a: true
            })
        </script>
        <style>
            .animate-enter, .animate-leave{
                width:100px;
                height:100px;
                background: #29b6f6;
                transition:all 2s;
                -moz-transition: all 2s; 
                -webkit-transition: all 2s;
                -o-transition:all 2s;
            }  
            .animate-enter-active, .animate-leave{
                width:300px;
                height:300px;
            }
            .animate-leave-active{
                width:100px;
                height:100px;
            }
        </style>
    </head>
    <body>
        <div :controller='ani' >
            <p><input type='button' value='click' :click='@a =!@a'></p>
            <div :effect="{is:'animate',action:@a}"></div>
        </div>
</body>
</html>
 * 
 */