2017年4月11日 星期二

(js)transitionJS....簡易版,除去 step, timer

////////////////////////////////////////////////////////////////////////////////
//
// 處理 transition 動畫
//
// (簡化版)
// 把每次的動畫屬性都設為 all
// 就不用處理 css 與 transitionProperty 間的對應
//
// 2017-03-30
//
//
// 最大的問題是 >>
// 如何得知 transition 是否順利執行
// transitionend 能否正確啟動
//
////////////////////////////////////////////////////////////////////////////////
if (typeof Area88_ == 'undefined') {
    if (typeof window.jQuery != 'undefined') {
        Area88_ = window.jQuery;
    } else {
        Area88_ = {};
    }
}
////////////////////////////////////////////////////////////////////////////////
(function ($) {


    // class
    // 處理動畫的類別
    function SetTransition(dom) {
        this.fn = SetTransition;
        this.dom = dom;

        // 使用者對動畫的設定
        this.setting = {};

        this.next = (typeof next === 'function' ? next : undefined);

        this.timeHandle = {
            delay: undefined,
            before: undefined,
            end: undefined
        };
        // 防止 endEvent 太早激發
        // false 為解鎖
        this.timeLock = false;

        // 備份 this.setting.css
        this.cssSetting = {};
    }
    /* ====================================================================== */
    (function () {
        // this._API = function(){};

        // 只能 stop 自身下的動畫命令
        this.stop = function (jumpToEnd) {

            if (!this.dom.dataset.area88Animate) {
                alert('no animation');
                // 元素已經沒在運動
                return;
            }
            this._resetEndEvent();
            this._resetTimeHandle();
            /* ---------------------------------- */
            var style = this.dom.style;

            if (jumpToEnd) {
                // 下面順序不能變換
                style.transition = 'none';

                $(this.dom).redraw_();

            } else {
                // 從 style 取得非 transition 的屬性與質
                // var styleProperty = this._getCurrentStyleProperty();

                // 運動中的動畫屬性
                var styleProperty = this.cssSetting;
                var css = getComputedStyle(this.dom, null);

                // 將 style 換成 現在的 css 值
                for (var key in styleProperty) {
                    if (styleProperty.hasOwnProperty(key)) {
                        styleProperty[key] = css[key] || '';
                    }
                }
                $(this.dom).redraw_();

                // 最重要的步驟
                $(this.dom).css(styleProperty);
            }
            /* ---------------------------------- */

            // 呼叫結束程序,但不執行 complete
            this.setting.complete = undefined;
            this._whenTransitionEnd();
        };
        /* ------------------------------------------------------------------ */
        this.animate = function (setting, next) {
            // debugger;

            this.next = (typeof next === 'function' ? next : undefined);
            this.setting = ($.isPlainObject(setting) ? setting : {});

            // 過濾 this.setting.css 的設定
            // 不能留有 transition 相關的設定
            this._cssSetFilter();

            /* ---------------------------------- */
            if (!this._willAnimate()) {
                // 若不符合動畫條件

                console.log('cant animate');
                this._animateNext();
                return;
            }

            if(this.dom.dataset.area88Animate){
                console.dir(this.setting.css);
                console.dir(this.cssSetting);
                console.log('-----------------');
                // 若在移動中,又下命令
                // 需判斷與先前的 style 設定是否相符
                // 若完全相符,就不用往下
                if(!this._willContinueAnimate()){
                    console.log('the same before .... no new setting');
                    return;
                }
            }
            /* ================================== */
            // 以下為確定動畫

            // 複製 cssSetting
            this.cssSetting = $.extend({}, this.setting.css);

            // 前置動作
            // 清理之前檢查(transitionEnd)的計時器
            // 若不用 queue 在第一個動作尚未結束時,加入第2個動作
            // 第一個的檢查計時器的時間將不對
            this._resetTimeHandle();

            this._resetEndEvent();


            if (typeof this.setting.start === 'function') {
                this.setting.start.call(this.dom);
            }


            // dom 未隱藏,且有設置動畫時間
            // 執行運動
            // 沒有設置動畫時間,就不會有動畫

            if (this.setting.delay && !this.next) {
                // 非隊列,有設定 delay
                this.fn.delayTimeHandle = setTimeout(function () {
                    this._animate();
                }.bind(this), this.setting.delay);
            } else {
                this._animate();
            }

        };
    }).call(SetTransition.prototype);
    /* ====================================================================== */
    (function () {
        // 執行運動
        this._animate = function () {
            var self = this;
            // 符合動畫條件
            // 若有設置顯示時間
            // 若有顯示
            // 若 opacity > 0

            this._setTansition();

            this._bindEvent();
            /* ---------------------------------- */
            // 計時器

            var time_1 = this.setting.duration;
            var time_2 = this.setting.duration + 50;

            // 預防 endEvent 太早起動
            this.timeLock = true;

            this.timeHandle.before = setTimeout(function () {
                self.timeHandle.before = undefined;
                // 開鎖
                self.timeLock = false;
                // alert('unlock');
            }, time_1);

            // 預防 endEvent 過時沒啟動
            this.timeHandle.end = setTimeout(function () {
                // alert('check');
                self.timeHandle.end = undefined;

                // 觸發 endEvent
                self._trigger($.support.transitionEnd);
            }, time_2);

            /* ---------------------------------- */
            // 標注動畫中
            // 才會知道正在動畫中
            this.dom.dataset.area88Animate = (this.next ? 'queue' : 'noqueue');

            $(this.dom).redraw_();

            // 下面開始動畫
            $(this.dom).css(this.cssSetting);
        };
        /* ------------------------------------------------------------------ */
        // animate end
        this._animateNext = function () {

            this.setting = {};

            if (typeof this.next === 'function') {
                this.next();
            } else {
                return;
            }
        };
    }).call(SetTransition.prototype);
    /* ====================================================================== */
    (function () {
        // $.tsEndCheck_ 會設置計時器來檢查 transitionEnd 是否正常執行
        // 在每次執行前,要清除上次設定的計時器
        // 刪除之前(tsEndCheck_)的檢查
        this._resetTimeHandle = function () {

            clearTimeout(this.timeHandle.before);
            clearTimeout(this.timeHandle.end);
            clearTimeout(this.timeHandle.delay);

            this.timeHandle.before = undefined;
            this.timeHandle.end = undefined;
            this.timeHandle.delay = undefined;
        };
        /* ------------------------------------------------------------------ */

        // 移除之前設定的事件
        // 萬一之前的(transition)尚未完成
        // 又突然給一個命令
        // 會造成之前的一些註冊事件重複發生
        this._resetEndEvent = function () {
            // transitionEnd 會執行的任務
            $(this.dom).off($.support.transitionEnd + '.area88.complete');
        };
        /* ------------------------------------------------------------------ */
        // 設置 transition
        this._setTansition = function () {
            // debugger;
            var cssEase = $.transitionOption_.cssEase;


            if (typeof cssEase[this.setting.easing] === 'string') {
                this.setting.easing = cssEase[this.setting.easing];
            }

            var set = 'all';

            // duration
            set += ' ' + ((this.setting.duration / 1000) + 's');

            // easing
            set += ' ' + this.setting.easing;

            // delay
            set += ' 0s';

            this.dom.style[$.support.transition] = set;
        };
        /* ------------------------------------------------------------------ */
        this._whenTransitionEnd = function () {
            // 清除 endEvent check
            clearTimeout(this.timeHandle.end);
            this.timeHandle.end = undefined;

            $(this.dom).off($.support.transitionEnd + '.area88.complete');

            // 去除動畫標記
            // 表示動畫結束
            delete this.dom.dataset.area88Animate;

            // 還原原本的 transition 設置
            this._resetTransitionSetting();

            /* -----------------------------------*/
            if (typeof this.setting.complete === 'function') {
                // complete
                // alert(self.setting.complete.toString());
                this.setting.complete.call(self.dom);
            }
            /* -----------------------------------*/
            this._animateNext();
        };
        /* ------------------------------------------------------------------ */
        /**
         * 綁定 transitionEnd 事件,才能順利驅動 complete
         * @return [type] [description]
         */
        this._bindEvent = function () {
            var self = this;

            // 綁定(transitionend.area88.complete)事件
            $(this.dom).on($.support.transitionEnd + '.area88.complete', function (e) {
                // alert('fire: ' + self.fn.timeLock);
                if (self.timeLock) {
                    // 時間鎖未打開,太早起動
                    // e.stopPropagation();
                    return;
                }

                self._whenTransitionEnd();
            });
        };

        /* ------------------------------------------------------------------ */
        // 過濾 this.setting.css 的設定
        this._cssSetFilter = function () {
            // debugger;

            var css2TransitionMap = $.transitionOption_.css2TransitionMap;

            var keyList = Object.keys(this.setting.css);

            keyList.forEach(function (key) {
                // debugger;

                this.setting.css[key] = this.setting.css[key].trim();

                // 不能在css裡置動畫屬性
                if (/transition/gi.test(key)) {
                    delete this.setting.css[key];
                }
                /* ---------------------------------- */
                // 若是諸如 padding, margin 之類
                // 必須轉換,主要方便與 css 判別,與 stop
                // padding -> padding-top, padding-right.......
                // style.padding 可能在 css 找不到正確數質...無法 stop
                if (typeof css2TransitionMap[key] !== 'undefined') {
                    var tmpValue = this.setting.css[key];
                    delete this.setting.css[key];

                    css2TransitionMap[key].forEach(function (_key) {
                        this.setting.css[_key] = tmpValue;
                    });
                }
            }, this);
        };
    }).call(SetTransition.prototype);
    /* ====================================================================== */
    (function () {
        // 判斷是否合乎動畫規則
        // 若 cssSetting 的數質等於當前
        // 若隱藏起來
        this._willAnimate = function () {
            // debugger;

            var self = this,
                css = getComputedStyle(this.dom, null);

            var res = [];

            // 是否隱藏中
            if (/none/gi.test(css.display)) {
                res.push(false);
            }
            /* ---------------------------------- */
            // duration <= 0 不能動畫
            if (this.setting.duration <= 0) {
                res.push(false);
            }
            /* ---------------------------------- */
            // 設定的數質是否與當前不同 syle => css
            // 要不同才能動畫


            var judge_1 = (function (self) {
                // debugger;

                for (var key in self.setting.css) {
                    // 紀錄 cssSetting 對應當前的 css 數值

                    var styleValue = self.setting.css[key];
                    var cssValue = css.getPropertyValue(key).trim();

                    var reg = new RegExp('^' + cssValue, 'i');

                    if (reg.test(styleValue)) {
                        continue;
                    } else {
                        // 有數值不同
                        return true;
                    }
                } // for-loop

                return false;
            })(this);

            res.push(judge_1);
            /* ---------------------------------- */


            return (res.indexOf(false) >= 0 ? false : true);
        };
        /* ------------------------------------------------------------------ */
        // 若 dom 正在移動中
        // 而 style 設定與上次相同
        // 也沒有必要設定動畫
        this._willContinueAnimate = function(){
            debugger;
            var judge_2 = (function (self) {
                var settingNum_1 = Object.keys(self.cssSetting).length;
                 var settingNum_2 = Object.keys(self.setting.css).length;

                if (settingNum_1 !== settingNum_2) {
                    // 若與上次 style 設定的數量不同
                    // 會動畫
                    return true;
                } else {

                    for (var key in self.setting.css) {
                        var nowValue = self.setting.css[key];
                        var prevValue = self.cssSetting[key];

                        if (/\+=|-=/i.test(nowValue)) {
                            // style 的設定有與上次不同
                            return true;
                        } else {
                            // 用現在的 style 設定,比對上次的 style 設定
                            var reg = new RegExp(('^' + nowValue), 'i');
                            if (reg.test(prevValue)) {
                                continue;
                            } else {
                                return true;
                            }
                        }
                    }

                    return false;
                }
            })(this);

            return judge_2;
        };
        /* ------------------------------------------------------------------ */
        // 處理 trigger 功能
        this._trigger = function (evenName, data) {
            try {
                $(this.dom).trigger_(evenName, data);
            } catch (error) {
                $(this.dom).trigger(evenName, data);
            }
        };
        /* ------------------------------------------------------------------ */
        // 清除動畫設定
        this._resetTransitionSetting = function () {
            var style = this.dom.style;

            style.transitionProperty = '';
            style.transitionDuration = '';
            style.transitionDelay = '';
            style.transitionTimingFunction = '';
            style.transition = '';
        };

    }).call(SetTransition.prototype);
    ////////////////////////////////////////////////////////////////////////////
    // 處理 .transition_() 進來的參數
    // 方法的重載
    function _getArguments(cssSetting, duration, easing, delay, callback) {
        // debugger;
        var option;

        var arg = $.makeArray(arguments).filter(function (value) {
            if (value == null) {
                return false;
            }
            return true;
        });

        if (!$.isPlainObject(cssSetting)) {
            throw new TypeError('arguments[0] must be plainObject');
            return;
        }

        if (arg.length == 2 && $.isPlainObject(duration)) {
            // .transition({}, option)
            option = duration;
            duration = undefined;
        } else {
            option = undefined;

            if (typeof duration === 'function') {
                callback = duration;
                duration = undefined;
            } else if (typeof easing === 'function') {
                // .transition({}, duration, callback)
                callback = easing;
                easing = undefined;
            } else if (typeof delay === 'function') {
                // .transition({}, duration, easing, callback)
                callback = delay;
                delay = undefined;
            } else if (typeof option === 'function') {
                // .transition({}, duration, easing, delay, callback)
                option = undefined;
                callback = option;
            }
        }
        /* ================================================================== */
        var res = {
            css: cssSetting,
            queue: true,
            duration: $.fx.speeds._default,
            easing: $.transitionOption_.cssEase._default,
            delay: 0,
            start: function () { },
            complete: function () { }
        };

        if ($.isPlainObject(option)) {
            res = $.extend({}, res, option);
        } else {
            res.duration = (typeof duration === 'number' ? duration : res.duration);
            res.easing = (typeof easing === 'string' ? easing : res.easing);
            res.delay = (typeof delay === 'number' ? delay : res.delay);
            res.complete = (typeof callback === 'function' ? callback : res.complete);
        }
        /* ---------------------------------- */
        // 檢查 easing
        var easingKeyList = Object.getOwnPropertyNames($.transitionOption_.cssEase);

        if (easingKeyList.indexOf(res.easing) < 0 && !/^cubic-bezier\(.+\)&/gi.test(res.easing)) {
            throw new TypeError('easing setting no support');
        }
        /* ---------------------------------- */
        return res;
    }
    /* ====================================================================== */
    // here
    $.fn.transition_ = function (cssSetting, duration, easing, delay, option, callback) {
        var self = this;

        var setting = _getArguments(cssSetting, duration, easing, delay, option, callback);

        this.each(function (index, dom) {
            setting.dom = dom;
            $.transition_(setting);
        });

        return this;
    };
    /* ====================================================================== */
    // here
    $.transition_ = function (dom, cssSetting, duration, easing, delay, option, callback) {

        var setting;
        var _cssSetting, _queue, _duration, _easing, _delay, _complete;

        if ($.isPlainObject(dom)) {
            // 內部傳送
            setting = dom;
            dom = setting.dom;
        } else {
            setting = _getArguments(cssSetting, duration, easing, delay, option, callback);
            setting.dom = dom;
        }
        /* ---------------------------------- */
        // debugger;
        var xman;

        if (xman = $(dom).data('_setTransition_')) { } else {
            xman = new SetTransition(dom);
            $(dom).data('_setTransition_', xman);
        }

        if (setting.queue) {
            // 若要設置 queue
            if (setting.delay) {
                // 若有設置 delay
                $(dom).delay(setting.delay);
            }

            $(dom).queue(function (next) {
                xman.animate(setting, next);
            });
        } else {
            // 若不用 queue

            if (setting.delay) {
                setTimeout(function () {
                    xman.animate(setting);
                }, setting.delay);
            } else {
                xman.animate(setting);
            }
        }
    };
    ////////////////////////////////////////////////////////////////////////////
    ////////////////////////////////////////////////////////////////////////////

    /**
     * @param {string} queue: 隊列的名稱
     */
    $.fn.transition_stop_ = function (queue, clearQueue, jumpToEnd) {

        return this.each(function (i, dom) {
            $.transition_stop_(dom, queue, clearQueue, jumpToEnd);
        });
    };
    /* ---------------------------------------------------------------------- */
    /**
     * 必須清除計時器
     * 清除 transiotnEnd.area88.complete 事件
     * 清除 delay 計時器
     *
     * @param  {[type]} dom        [description]
     * @param  {[type]} queue      [queueName]
     * @param  {[type]} clearQueue [是否要清除 queue]
     * @param  {[type]} jumpToEnd  [是否直接跳到設定值]
     * @return [type]              [description]
     */
    $.transition_stop_ = function (dom, queue, clearQueue, jumpToEnd) {
        // 清空隊列


        if (typeof queue === 'string') {
            // ( [queue ] [, clearQueue ] [, jumpToEnd ] )
            clearQueue = clearQueue || false;
            jumpToEnd = jumpToEnd || false;
        } else {
            // ( [clearQueue ] [, jumpToEnd ] )
            queue = 'fx';
            jumpToEnd = clearQueue || false;
            clearQueue = queue || false;
        }
        /* ---------------------------------- */
        if (clearQueue) {
            $(dom).clearQueue(queue);
        }
        /* ---------------------------------- */
        var xman = $(dom).data('_setTransition_');
        // 暫停
        if (jumpToEnd) {
            xman && (xman.stop(true));
        } else {
            xman && (xman.stop());
        }
    }
})(Area88_);
////////////////////////////////////////////////////////////////////////////////