Source: waitress/progress.js

/**
 * Progress.js v0.1.0
 * https://github.com/usablica/progress.js
 * MIT licensed
 *
 * Copyright (C) 2013 usabli.ca - Afshin Mehrabani (@afshinmeh)
 */

(function (root, factory) {
  if (typeof exports === "object") {
    // CommonJS
    factory(exports);
  } else if (typeof define === "function" && define.amd) {
    // AMD. Register as an anonymous module.
    define(["exports"], factory);
  } else {
    // Browser globals
    factory(root);
  }
})(this, function (exports) {
  //Default config/variables
  var VERSION = "0.1.0";

  /**
   * ProgressJs main class
   *
   * @class ProgressJs
   */
  function ProgressJs(obj) {
    if (typeof obj.length != "undefined") {
      this._targetElement = obj;
    } else {
      this._targetElement = [obj];
    }

    if (typeof window._progressjsId === "undefined") window._progressjsId = 1;

    if (typeof window._progressjsIntervals === "undefined")
      window._progressjsIntervals = {};

    this._options = {
      //progress bar theme
      theme: "blue",
      //overlay mode makes an overlay layer in the target element
      overlayMode: false,
      //to consider CSS3 transitions in events
      considerTransition: true,
    };
  }

  /**
   * Start progress for specific element(s)
   *
   * @api private
   * @method _createContainer
   */
  function _startProgress() {
    //call onBeforeStart callback
    if (typeof this._onBeforeStartCallback != "undefined") {
      this._onBeforeStartCallback.call(this);
    }

    //create the container for progress bar
    _createContainer.call(this);

    for (
      var i = 0, elmsLength = this._targetElement.length;
      i < elmsLength;
      i++
    ) {
      _setProgress.call(this, this._targetElement[i]);
    }
  }

  /**
   * Set progress bar for specific element
   *
   * @api private
   * @method _setProgress
   * @param {Object} targetElement
   */
  function _setProgress(targetElement) {
    //if the target element already as `data-progressjs`, ignore the init
    if (targetElement.hasAttribute("data-progressjs")) return;

    //get target element position
    var targetElementOffset = _getOffset.call(this, targetElement);

    targetElement.setAttribute("data-progressjs", window._progressjsId);

    var progressElementContainer = document.createElement("div");
    progressElementContainer.className =
      "progressjs-progress progressjs-theme-" + this._options.theme;

    //set the position percent elements, it depends on targetElement tag
    if (targetElement.tagName.toLowerCase() === "body") {
      progressElementContainer.style.position = "fixed";
    } else {
      progressElementContainer.style.position = "absolute";
    }

    progressElementContainer.setAttribute(
      "data-progressjs",
      window._progressjsId,
    );
    var progressElement = document.createElement("div");
    progressElement.className = "progressjs-inner";

    //create an element for current percent of progress bar
    var progressPercentElement = document.createElement("div");
    progressPercentElement.className = "progressjs-percent";
    progressPercentElement.innerHTML = "1%";

    progressElement.appendChild(progressPercentElement);

    if (
      this._options.overlayMode &&
      targetElement.tagName.toLowerCase() === "body"
    ) {
      //if we have `body` for target element and also overlay mode is enable, we should use a different
      //position for progress bar container element
      progressElementContainer.style.left = 0;
      progressElementContainer.style.right = 0;
      progressElementContainer.style.top = 0;
      progressElementContainer.style.bottom = 0;
    } else {
      //set progress bar container size and offset
      progressElementContainer.style.left = targetElementOffset.left + "px";
      progressElementContainer.style.top = targetElementOffset.top + "px";
      //if targetElement is body set to percent so it scales with browser resize
      if (targetElement.nodeName == "BODY") {
        progressElementContainer.style.width = "100%";
      } else {
        progressElementContainer.style.width = targetElementOffset.width + "px";
      }

      if (this._options.overlayMode) {
        progressElementContainer.style.height =
          targetElementOffset.height + "px";
      }
    }

    progressElementContainer.appendChild(progressElement);

    //append the element to container
    var container = document.querySelector(".progressjs-container");
    container.appendChild(progressElementContainer);

    _setPercentFor(targetElement, 1);

    //and increase the progressId
    ++window._progressjsId;
  }

  /**
   * Set percent for all elements
   *
   * @api private
   * @method _setPercent
   * @param {Number} percent
   */
  function _setPercent(percent) {
    for (
      var i = 0, elmsLength = this._targetElement.length;
      i < elmsLength;
      i++
    ) {
      _setPercentFor.call(this, this._targetElement[i], percent);
    }
  }

  /**
   * Set percent for specific element
   *
   * @api private
   * @method _setPercentFor
   * @param {Object} targetElement
   * @param {Number} percent
   */
  function _setPercentFor(targetElement, percent) {
    var self = this;

    //prevent overflow!
    if (percent >= 100) percent = 100;

    if (targetElement.hasAttribute("data-progressjs")) {
      //setTimeout for better CSS3 animation applying in some cases
      setTimeout(function () {
        //call the onprogress callback
        if (typeof self._onProgressCallback != "undefined") {
          self._onProgressCallback.call(self, targetElement, percent);
        }

        var percentElement = _getPercentElement(targetElement);
        percentElement.style.width = parseInt(percent) + "%";

        var percentElement = percentElement.querySelector(
          ".progressjs-percent",
        );
        var existingPercent = parseInt(
          percentElement.innerHTML.replace("%", ""),
        );

        //start increase/decrease the percent element with animation
        (function (percentElement, existingPercent, currentPercent) {
          var increasement = true;
          if (existingPercent > currentPercent) {
            increasement = false;
          }

          var intervalIn = 10;
          function changePercentTimer(
            percentElement,
            existingPercent,
            currentPercent,
          ) {
            //calculate the distance between two percents
            var distance = Math.abs(existingPercent - currentPercent);
            if (distance < 3) {
              intervalIn = 30;
            } else if (distance < 20) {
              intervalIn = 20;
            } else {
              intervanIn = 1;
            }

            if (existingPercent - currentPercent != 0) {
              //set the percent
              percentElement.innerHTML =
                (increasement ? ++existingPercent : --existingPercent) + "%";
              setTimeout(function () {
                changePercentTimer(
                  percentElement,
                  existingPercent,
                  currentPercent,
                );
              }, intervalIn);
            }
          }

          changePercentTimer(percentElement, existingPercent, currentPercent);
        })(percentElement, existingPercent, parseInt(percent));
      }, 50);
    }
  }

  /**
   * Get the progress bar element
   *
   * @api private
   * @method _getPercentElement
   * @param {Object} targetElement
   */
  function _getPercentElement(targetElement) {
    var progressjsId = parseInt(targetElement.getAttribute("data-progressjs"));
    return document.querySelector(
      '.progressjs-container > .progressjs-progress[data-progressjs="' +
        progressjsId +
        '"] > .progressjs-inner',
    );
  }

  /**
   * Auto increase the progress bar every X milliseconds
   *
   * @api private
   * @method _autoIncrease
   * @param {Number} size
   * @param {Number} millisecond
   */
  function _autoIncrease(size, millisecond) {
    var self = this;

    var target = this._targetElement[0];
    if (!target) return;
    var progressjsId = parseInt(target.getAttribute("data-progressjs"));

    if (typeof window._progressjsIntervals[progressjsId] != "undefined") {
      clearInterval(window._progressjsIntervals[progressjsId]);
    }
    window._progressjsIntervals[progressjsId] = setInterval(function () {
      _increasePercent.call(self, size);
    }, millisecond);
  }

  /**
   * Increase the size of progress bar
   *
   * @api private
   * @method _increasePercent
   * @param {Number} size
   */
  function _increasePercent(size) {
    for (
      var i = 0, elmsLength = this._targetElement.length;
      i < elmsLength;
      i++
    ) {
      var currentElement = this._targetElement[i];
      if (currentElement.hasAttribute("data-progressjs")) {
        var percentElement = _getPercentElement(currentElement);
        var existingPercent = parseInt(
          percentElement.style.width.replace("%", ""),
        );
        if (existingPercent) {
          _setPercentFor.call(
            this,
            currentElement,
            existingPercent + (size || 1),
          );
        }
      }
    }
  }

  /**
   * Close and remove progress bar
   *
   * @api private
   * @method _end
   */
  function _end() {
    //call onBeforeEnd callback
    if (typeof this._onBeforeEndCallback != "undefined") {
      if (this._options.considerTransition === true) {
        //we can safety assume that all layers would be the same, so `this._targetElement[0]` is the same as `this._targetElement[1]`
        _getPercentElement(this._targetElement[0]).addEventListener(
          whichTransitionEvent(),
          this._onBeforeEndCallback,
          false,
        );
      } else {
        this._onBeforeEndCallback.call(this);
      }
    }

    var target = this._targetElement[0];
    if (!target) return;
    var progressjsId = parseInt(target.getAttribute("data-progressjs"));

    for (
      var i = 0, elmsLength = this._targetElement.length;
      i < elmsLength;
      i++
    ) {
      var currentElement = this._targetElement[i];
      var percentElement = _getPercentElement(currentElement);

      if (!percentElement) return;

      var existingPercent = parseInt(
        percentElement.style.width.replace("%", ""),
      );

      var timeoutSec = 1;
      if (existingPercent < 100) {
        _setPercentFor.call(this, currentElement, 100);
        timeoutSec = 500;
      }

      //I believe I should handle this situation with eventListener and `transitionend` event but I'm not sure
      //about compatibility with IEs. Should be fixed in further versions.
      (function (percentElement, currentElement) {
        setTimeout(function () {
          percentElement.parentNode.className += " progressjs-end";

          setTimeout(function () {
            //remove the percent element from page
            percentElement.parentNode.parentNode.removeChild(
              percentElement.parentNode,
            );
            //and remove the attribute
            currentElement.removeAttribute("data-progressjs");
          }, 1000);
        }, timeoutSec);
      })(percentElement, currentElement);
    }

    //clean the setInterval for autoIncrease function
    if (window._progressjsIntervals[progressjsId]) {
      //`delete` keyword has some problems in IE
      try {
        clearInterval(window._progressjsIntervals[progressjsId]);
        window._progressjsIntervals[progressjsId] = null;
        delete window._progressjsIntervals[progressjsId];
      } catch (ex) {}
    }
  }

  /**
   * Remove progress bar without finishing
   *
   * @api private
   * @method _kill
   */
  function _kill() {
    var target = this._targetElement[0];
    if (!target) return;
    var progressjsId = parseInt(target.getAttribute("data-progressjs"));

    for (
      var i = 0, elmsLength = this._targetElement.length;
      i < elmsLength;
      i++
    ) {
      var currentElement = this._targetElement[i];
      var percentElement = _getPercentElement(currentElement);

      if (!percentElement) return;

      //I believe I should handle this situation with eventListener and `transitionend` event but I'm not sure
      //about compatibility with IEs. Should be fixed in further versions.
      (function (percentElement, currentElement) {
        percentElement.parentNode.className += " progressjs-end";

        setTimeout(function () {
          //remove the percent element from page
          percentElement.parentNode.parentNode.removeChild(
            percentElement.parentNode,
          );
          //and remove the attribute
          currentElement.removeAttribute("data-progressjs");
        }, 1000);
      })(percentElement, currentElement);
    }

    //clean the setInterval for autoIncrease function
    if (window._progressjsIntervals[progressjsId]) {
      //`delete` keyword has some problems in IE
      try {
        clearInterval(window._progressjsIntervals[progressjsId]);
        window._progressjsIntervals[progressjsId] = null;
        delete window._progressjsIntervals[progressjsId];
      } catch (ex) {}
    }
  }

  /**
   * Create the progress bar container
   *
   * @api private
   * @method _createContainer
   */
  function _createContainer() {
    //first check if we have an container already, we don't need to create it again
    if (!document.querySelector(".progressjs-container")) {
      var containerElement = document.createElement("div");
      containerElement.className = "progressjs-container";
      document.body.appendChild(containerElement);
    }
  }

  /**
   * Get an element position on the page
   * Thanks to `meouw`: http://stackoverflow.com/a/442474/375966
   *
   * @api private
   * @method _getOffset
   * @param {Object} element
   * @returns Element's position info
   */
  function _getOffset(element) {
    var elementPosition = {};

    if (element.tagName.toLowerCase() === "body") {
      //set width
      elementPosition.width = element.clientWidth;
      //set height
      elementPosition.height = element.clientHeight;
    } else {
      //set width
      elementPosition.width = element.offsetWidth;
      //set height
      elementPosition.height = element.offsetHeight;
    }

    //calculate element top and left
    var _x = 0;
    var _y = 0;
    while (element && !isNaN(element.offsetLeft) && !isNaN(element.offsetTop)) {
      _x += element.offsetLeft;
      _y += element.offsetTop;
      element = element.offsetParent;
    }
    //set top
    elementPosition.top = _y;
    //set left
    elementPosition.left = _x;

    return elementPosition;
  }

  /**
   * Overwrites obj1's values with obj2's and adds obj2's if non existent in obj1
   * via: http://stackoverflow.com/questions/171251/how-can-i-merge-properties-of-two-javascript-objects-dynamically
   *
   * @param obj1
   * @param obj2
   * @returns obj3 a new object based on obj1 and obj2
   */
  function _mergeOptions(obj1, obj2) {
    var obj3 = {};
    for (var attrname in obj1) {
      obj3[attrname] = obj1[attrname];
    }
    for (var attrname in obj2) {
      obj3[attrname] = obj2[attrname];
    }
    return obj3;
  }

  var progressJs = function (targetElm) {
    if (typeof targetElm === "object") {
      //Ok, create a new instance
      return new ProgressJs(targetElm);
    } else if (typeof targetElm === "string") {
      //select the target element with query selector
      var targetElement = document.querySelectorAll(targetElm);

      if (targetElement) {
        return new ProgressJs(targetElement);
      } else {
        throw new Error("There is no element with given selector.");
      }
    } else {
      return new ProgressJs(document.body);
    }
  };

  /**
   * Get correct transition callback
   * Thanks @webinista: http://stackoverflow.com/a/9090128/375966
   *
   * @returns transition name
   */
  function whichTransitionEvent() {
    var t;
    var el = document.createElement("fakeelement");
    var transitions = {
      transition: "transitionend",
      OTransition: "oTransitionEnd",
      MozTransition: "transitionend",
      WebkitTransition: "webkitTransitionEnd",
    };

    for (t in transitions) {
      if (el.style[t] !== undefined) {
        return transitions[t];
      }
    }
  }

  /**
   * Current ProgressJs version
   *
   * @property version
   * @type String
   */
  progressJs.version = VERSION;

  //Prototype
  progressJs.fn = ProgressJs.prototype = {
    clone: function () {
      return new ProgressJs(this);
    },
    setOption: function (option, value) {
      this._options[option] = value;
      return this;
    },
    setOptions: function (options) {
      this._options = _mergeOptions(this._options, options);
      return this;
    },
    start: function () {
      _startProgress.call(this);
      return this;
    },
    set: function (percent) {
      _setPercent.call(this, percent);
      return this;
    },
    increase: function (size) {
      _increasePercent.call(this, size);
      return this;
    },
    autoIncrease: function (size, millisecond) {
      _autoIncrease.call(this, size, millisecond);
      return this;
    },
    end: function () {
      _end.call(this);
      return this;
    },
    kill: function () {
      _kill.call(this);
      return this;
    },
    onbeforeend: function (providedCallback) {
      if (typeof providedCallback === "function") {
        this._onBeforeEndCallback = providedCallback;
      } else {
        throw new Error("Provided callback for onbeforeend was not a function");
      }
      return this;
    },
    onbeforestart: function (providedCallback) {
      if (typeof providedCallback === "function") {
        this._onBeforeStartCallback = providedCallback;
      } else {
        throw new Error(
          "Provided callback for onbeforestart was not a function",
        );
      }
      return this;
    },
    onprogress: function (providedCallback) {
      if (typeof providedCallback === "function") {
        this._onProgressCallback = providedCallback;
      } else {
        throw new Error("Provided callback for onprogress was not a function");
      }
      return this;
    },
  };

  exports.progressJs = progressJs;
  return progressJs;
});