countdown.js 8.91 KB
/**
 * countdown.js.
 * @author hgwang
 * @date 2016-05-29
 */
'use strict';
var $ = require('yoho-jquery');
var EVENT_AFTER_PAINT = 'afterPaint';
var defaultOPtions = {
    el: {},

    // unix时间戳,单位应该是毫秒!
    stopPoint: 0,
    leftTime: 0,
    template: '', // '${h}时${m}分${s-ext}秒'
    varRegular: /\$\{([\-\w]+)\}/g,
    clock: ['d', 100, 2, 'h', 24, 2, 'm', 60, 2, 's', 60, 2, 'u', 10, 1],
    effect: 'normal'
};
var effect = {
    normal: {
        paint: function() {
            var me = this,
                content;

            // 找到值发生改变的hand
            $.each(me.hands, function(index, hand) {
                if (hand.lastValue !== hand.value) {
                    // 生成新的markup
                    content = '';

                    $.each(me._toDigitals(hand.value, hand.bits), function(i, digital) {
                        content += me._html(digital, '', 'digital');
                    });

                    // 并更新
                    hand.node.html(content);
                }
            });
        }
    }
};
var timer = (function() {
    var fns = [],
        commands = [];// 操作指令

    /**
     * timer
     * 调用频率为100ms一次。努力精确计时,调用帧函数
     */
    function timerIn() {
        // 计算新时间,调整diff
        var diff = +new Date() - timerIn.nextTime,
            count = 1 + Math.floor(diff / 100);

        // 循环处理fns二元组
        var frequency, step,
            i, len;

        // 为避免循环时受到 对fns数组操作 的影响,
        // add/remove指令提前统一处理
        while (commands.length) {
            commands.shift()();
        }

        diff = 100 - diff % 100;
        timerIn.nextTime += 100 * count;


        for (i = 0, len = fns.length; i < len; i += 2) {
            frequency = fns[i + 1];

            // 100次/s的
            if (frequency === 0) {
                fns[i](count);

                // 1000次/s的
            } else {
                // 先把末位至0,再每次加2
                frequency += 2 * count - 1;

                step = Math.floor(frequency / 20);
                if (step > 0) {
                    fns[i](step);
                }

                // 把末位还原成1
                fns[i + 1] = frequency % 20 + 1;
            }
        }

        // next
        setTimeout(timerIn, diff);
    }

    // 首次调用
    timerIn.nextTime = +new Date();
    timerIn();
    function indexOf(item, arr) {
        var i, len;

        for (i = 0, len = arr.length; i < len; ++i) {
            if (arr[i] === item) {
                return i;
            }
        }
        return -1;
    }

    return {
        add: function(fn, frequency) {
            commands.push(function() {
                fns.push(fn);
                fns.push(frequency === 1000 ? 1 : 0);
            });
        },
        remove: function(fn) {
            var i;

            commands.push(function() {
                i = indexOf(fn, fns);
                if (i !== -1) {
                    fns.splice(indexOf(fn, fns), 2);
                }
            });
        }
    };
}());

function Countdown(config) {
    var cfg;

    if (!(this instanceof Countdown)) {
        return new Countdown(config);
    }

    config.el = $(config.el);
    if (!config.el) {
        return;
    }

    cfg = config.el.attr('data-config');

    if (cfg) {
        cfg = JSON.parse(cfg.replace(/'/g, '"'));
        config = $.extend(true, {}, defaultOPtions, cfg, config);
    }

    this.config = config;
    this._init();
}
$.extend(Countdown.prototype, {
    /**
     * 初始化
     * @private
     */
    _init: function() {
        var me = this;
        var el = me.config.el;

        // 初始化时钟.
        var hands = [];

        // 分析markup
        var tmpl = el.html();
        var varRE = me.config.varRegular;

        var clock;
        var _reflow;

        /**
         * 指针结构
         * hand: {
         *   type: string,
         *   value: number,
         *   lastValue: number,
         *   base: number,
         *   radix: number,
         *   bits: number,
         *   node: S.Node
         * }
         */
        me.hands = hands;
        me.frequency = 1000;
        me._notify = [];

        varRE.lastIndex = 0;
        el.html(tmpl.replace(varRE, function(str, type) {
            // 生成hand的markup
            var content = '';

            // 时钟频率校正.
            if (type === 'u' || type === 's-ext') {
                me.frequency = 100;
            }

            if (type === 's-ext') {
                hands.push({type: 's'});
                hands.push({type: 'u'});
                content = me._html('', 's', 'handlet') +
                    me._html('.', '', 'digital') +
                    me._html('', 'u', 'handlet');
            } else {
                hands.push({type: type});
            }

            return me._html(content, type, 'hand');
        }));

        // 指针type以外属性(node, radix, etc.)的初始化.
        clock = me.config.clock;

        $.each(hands, function(index, hand) {
            var type = hand.type,
                base = 100, i;

            hand.node = el.find('.hand-' + type);

            // radix, bits 初始化.
            for (i = clock.length - 3; i > -1; i -= 3) {
                if (type === clock[i]) {
                    break;
                }

                base *= clock[i + 1];
            }
            hand.base = base;
            hand.radix = clock[i + 1];
            hand.bits = clock[i + 2];
        });

        me._getLeft();
        me._reflow();

        // bind reflow to me.
        _reflow = me._reflow;
        me._reflow = function() {
            return _reflow.apply(me, arguments);
        };
        timer.add(me._reflow, me.frequency);

        // 显示时钟.
        el.show();
    },

    /**
     * 获取倒计时剩余帧数
     */
    _getLeft: function() { // {{{
        var left = this.config.leftTime * 1000;
        var end = this.config.stopPoint;        // 这个是UNIX时间戳,毫秒级

        if (!left && end) {
            left = end - (+new Date());
        }

        this.left = left - left % this.frequency;
    }, // }}}
    /**
     * 更新时钟
     */
    _reflow: function(count) {
        var me = this;

        count = count || 0;
        me.left = me.left - me.frequency * count;

        // 更新hands
        $.each(me.hands, function(index, hand) {
            hand.lastValue = hand.value;
            hand.value = Math.floor(me.left / hand.base) % hand.radix;
        });

        // 更新时钟.
        me._repaint();

        // notify
        if (me._notify[me.left]) {
            $.each(me._notify[me.left], function(index, callback) {
                callback.call(me);
            });
        }

        // notify 可能更新me.left
        if (me.left < 1) {
            timer.remove(me._reflow);
        }

        return me;
    },

    /**
     * 重绘时钟
     * @private
     */
    _repaint: function() {
        effect[this.config.effect].paint.apply(this);

        this.config.el.trigger(EVENT_AFTER_PAINT);
    },

    /**
     * 把值转换为独立的数字形式
     * @private
     * @param {number} value
     * @param {number} bits
     */
    _toDigitals: function(value, bits) {
        var digitals = [];

        value = value < 0 ? 0 : value;

        // 把时、分、秒等换算成数字.
        while (bits--) {
            digitals[bits] = value % 10;

            value = Math.floor(value / 10);
        }

        return digitals;
    },

    /**
     * 生成需要的html代码,辅助工具
     * @private
     * @param {string|Array.<string>} content
     * @param {string} className
     * @param {string} type
     */
    _html: function(content, className, type) {
        if ($.isArray(content)) {
            content = content.join('');
        }

        switch (type) {
            case 'hand':
                className = type + ' hand-' + className;
                break;
            case 'handlet':
                className = type + ' hand-' + className;
                break;
            case 'digital':
                if (content === '.') {
                    className = type + ' ' + type + '-point ' + className;
                } else {
                    className = type + ' ' + type + '-' + content + ' ' + className;
                }
                break;
            default:
                break;
        }

        return '<i class="' + className + '">' + content + '</i>';
    },

    /**
     * 倒计时事件
     * @param {number} time unit: second
     * @param {Function} callback
     */
    notify: function(time, callback) {
        var notifies;

        time = time * 1000;
        time = time - time % this.frequency;

        notifies = this._notify[time] || [];
        notifies.push(callback);
        this._notify[time] = notifies;

        return this;
    }
});
exports.Countdown = Countdown;