<div data-timer="12/25/2018 04:22:44" data-timer-finish=".first"></div>

<div data-timer="10/11/2018 14:39:35" data-timer-finish=".second"></div>

<div class="first">
  Первый блок таймера
</div>

<div class="second">
  Второй блок таймера
</div>
(function($) {
  'use strict';

  var plugin = {
    name: 'timer',
    version: '1.0.1',
    language: 'RU',
    data: {
      setting: 'setting',
      general: 'general'
    },
    template: {
      prefix: 'v-'
    }
  };

  var defaults = {
    date: Date.now() + 922e5,
    double: true,
    language: 'RU',
    store: function() {
      return {};
    },
    template: function(time, store) {
      var seconds = '<span>'+ time.seconds.value + ' ' + time.seconds.text +'</span>';
      var minutes = '<span>'+ time.minutes.value + ' ' + time.minutes.text +'</span>';
      var hours = '<span>'+ time.hours.value + ' ' + time.hours.text +'</span>';
      var days = '<span>'+ time.days.value + ' ' + time.days.text +'</span>';
      return '<div>'+ days+ ' : ' +hours+ ' : ' +minutes+ ' : ' +seconds +'</div>';
    },
    finish: function(store) {
      $(this).css('color', 'red');
    }
  };

  var methods = {}, user = {};

  methods.init = function($this, options, common) {
    var setting = function() {
      var params = $.extend(true, {}, defaults, options);
      params.date = new Date(params.date).getTime();
      params.language = params.language.toUpperCase();
      $this.data(plugin.data.setting, params);
      return params;
    }();

    var general = function() {
      var params = $.extend(true, {}, {
        date: Date.now(),
        context: $this.get(0),
        store: setting.store.call($this.get(0)),
        interval: 0,
        language: (function() {
          var lang = (function() {
            switch (setting.language) {
              case 'RU': return {
                declen: {
                  days: ['дней', 'день', 'дня'],
                  hours: ['часов', 'час', 'часа'],
                  minutes: ['минут', 'минута', 'минуты'],
                  seconds: ['секунд', 'секунда', 'секунды']
                }
              };
              case 'EN': return {
                declen: {
                  days: ['day', 'days'],
                  hours: ['hour', 'hours'],
                  minutes: ['minute', 'minutes'],
                  seconds: ['second', 'seconds']
                }
              };
            }
          }());
          return common.language ? $.extend(true, {}, lang, common.language) : lang;
        }())
      }, common);
      $this.data(plugin.data.general, params);
      return params;
    }();

    initTimer();

    general.interval = setInterval(initTimer, 1e3);

    function initTimer() {
      general.date = Date.now();
      var remaining = setting.date - general.date;
      general.time = makeTimeObject(remaining);
      if (remaining <= 0) {
        var html = setting.finish.call(general.context, general.store);
        if (html) $this.html(templateEngine(html, general.time, general.store));
        return clearInterval(general.interval);
      }
      var html = setting.template.call(general.context, general.time, general.store);
      if (html) $this.html(templateEngine(html, general.time, general.store));
    }

    function makeTimeObject(remaining) {
      var time = getDeclen(getDate(remaining));
      if (setting.double) {
        for (var item in time) {
          time[item].value = doubleDigits(time[item].value);
        }
      }
      return time;
    }

    function getDeclen(time) {
      for (var item in time) {
        time[item].text = getDeclenWord(general.language.declen[item], time[item].value, setting.language);
      }
      return time;
    }

    function getDate(time) {
      var date = {};
      date.seconds = {
        value: Math.floor(time / 1000)
      };
      date.minutes = {
        value: Math.floor(date.seconds.value / 60)
      };
      date.hours = {
        value: Math.floor(date.minutes.value / 60)
      };
      date.days = {
        value: Math.floor(date.hours.value / 24)
      };
      date.hours.value %= 24;
      date.minutes.value %= 60;
      date.seconds.value %= 60;
      return date;
    }
  };

  methods.set = function(key, value) {
    var setting = $(this).data(plugin.data.setting);
    switch (typeof key) {
      case 'string':
        if (key.includes('.')) {
          nestedObjectPath(setting, key, value);
        } else {
          setting[key] = value;
        }
        break;
      case 'object':
        setting = $.extend(true, {}, setting, key);
        break;
    }
    clearInterval($(this).data(plugin.data.general).interval);
    return methods.init($(this), setting, {
      type: 'update',
      method: 'set'
    });
  };

  methods.store = function(params) {
    var setting = $(this).data(plugin.data.setting);
    var store = setting.store.call(this);
    setting.store = function() {
      return $.extend(true, {}, store, params);
    };
    clearInterval($(this).data(plugin.data.general).interval);
    return methods.init($(this), setting, {
      type: 'update',
      method: 'store'
    });
  };

  methods.get = function(category, key) {
    var section = $(this).data(category);
    if (key) {
      if (key.includes('.')) {
        return nestedObjectPath(section, key);
      } else {
        return Object.getOwnPropertyDescriptor(section, key).value;
      }
    }
    return section;
  };

  methods.simple = function(date, template, options) {
    var setting = function() {
      var params = $(this).data(plugin.data.setting);
      var props = function() {
        if (!options) return {};
        return typeof options === 'function' ? {
          finish: options
        } : options;
      }();
      return $.extend(true, {}, params, props, {
        date: date,
        template: template
      });
    }.bind(this)();
    return methods.init($(this), setting, {
      type: 'init',
      method: 'simple'
    });
  };

  methods.destroy = function() {
    $(this).removeData(plugin.data.setting).fadeOut(200, function() {
      clearInterval($(this).data(plugin.data.general).interval);
      $(this).empty().show();
    });
  };

  methods.version = function() {
    return plugin.version;
  };

  function nestedObjectPath(object, key, value) {
    var path = key.split('.');
    var get = function(path, object) {
      return path.reduce(function(previous, current) {
        return previous[current];
      }, object);
    };
    if (value) {
      var way = path.pop();
      get(path, object)[way] = value;
      return object;
    } else {
      return get(path, object);
    }
  }

  function getDeclenWord(variants, value, lang) {
    if (!lang) lang = plugin.language;
    switch (lang.toUpperCase()) {
      case 'EN':
        return decEn(variants, value);
      case 'RU':
        return decRu(variants, value);
    }
  }

  function parseHTML(raw) {
    var page = document.implementation.createHTMLDocument();
    page.body.innerHTML = raw;
    return page.body;
  }

  function decRu(variants, s) {
    var index = s % 100;
    if (index >= 11 && index <= 14) {
      index = 0;
    } else {
      index = (index %= 10) < 5 ? (index > 2 ? 2 : index): 0;
    }
    return variants[index];
  }

  function decEn(variants, value) {
    return variants[value !== 1 ? 1 : 0];
  }

  function doubleDigits(value) {
    return value < 10 ? '0' + value : String(value);
  }

  function templateEngine(html, time, store) {
    if (!html) return '';
    var $html = $(parseHTML(html));
    var prefix = plugin.template.prefix;
    var template = {
      show: prefix + 'show',
      if: prefix + 'if',
      else: prefix + 'else',
      hide: prefix + 'hide'
    };
    if ($('['+ template.show +']', $html).length) {
      $.each($('['+ template.show +']', $html), function() {
        var key = $(this).attr(template.show);
        if (key.includes('.')) {
          if (!nestedObjectPath(store, key)) {
            $(this).remove();
          }
        } else {
          if (!store[key]) {
            $(this).remove();
          }
        }
        $(this).removeAttr(template.show);
      });
    }
    if ($('['+ template.if +']', $html).length) {
      $.each($('['+ template.if +']', $html), function() {
        var value = $(this).attr(template.if);
        var $else = $(this).next('['+ template.else +']');
        if (['false', 'null', 'undefined', ''].includes(value)) {
          $else.removeAttr(template.else);
          $(this).remove();
        } else {
          if (value.includes('.')) {
            var nested = nestedObjectPath(store, value);
            if (nested !== undefined) {
              if (nested) {
                $(this).removeAttr(template.if);
                $else.remove();
              } else {
                $else.removeAttr(template.else);
                $(this).remove();
              }
            } else {
              $(this).removeAttr(template.if);
              $else.remove();
            }
          } else {
            if (store[value] !== undefined) {
              if (store[value]) {
                $(this).removeAttr(template.if);
                $else.remove();
              } else {
                $else.removeAttr(template.else);
                $(this).remove();
              }
            } else {
              $(this).removeAttr(template.if);
              $else.remove();
            }
          }
        }
      });
    }
    if ($('['+ template.hide +']', $html).length) {
      $.each($('['+ template.hide +']', $html), function() {
        $(this).hide().removeAttr(template.hide);
      });
    }
    return $html.children().length === 0 ? $html.html() : $html.children().wrap();
  }

  user[plugin.name] = function(method) {
    var params = Array.prototype.slice.call(arguments, 1);
    switch (method.toLowerCase()) {
      case 'declen':
        return getDeclenWord.apply(null, params);
      case 'version':
        return methods.version();
    }
  };

  $.extend(user);

  $.fn[plugin.name] = function(setting) {
    if (typeof setting === 'object' || !setting) {
      return $.each(this, function() {
        methods.init($(this), setting, {
          type: 'init',
          method: 'init'
        });
      });
    } else if (typeof setting === 'string') {
      var params = Array.prototype.slice.call(arguments, 1);
      var context = this.get(0);
      switch(setting.toLowerCase()) {
        case 'set':
          return methods.set.apply(context, params);
        case 'store':
          return methods.store.apply(context, params);
        case 'get':
          return methods.get.apply(context, params);
        case 'destroy':
          return methods.destroy.call(context);
        case 'version':
          return methods.version();
      }
    } else if (typeof arguments[1] === 'function') {
      return methods.simple.apply(this, arguments);
    }
  };

})(jQuery);



// --------------------

$.each($('[data-timer]'), function() {
  $(this).timer({
    date: $(this).attr('data-timer'),
    language: 'RU',
    template: function(time) {
      return `
        <span>
          Осталось
          ${time.hours.value} ${time.hours.text},
          ${time.minutes.value} ${time.minutes.text} и 
          <span v-if="${time.seconds.value > 5}">
            ${time.seconds.value} ${time.seconds.text}
          </span>
          <span v-else style="color:red;">
            ${time.seconds.value} ${time.seconds.text}
          </span>
        </span>
      `;
    },
    finish: function() {
      $($(this).attr('data-timer-finish')).fadeOut();
      return 'Время вышло!';
    }
  });
});

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js