<commit_before>/*
 * This var has to be outside the angular factory, otherwise when
 * there are multiple material apps on the same page, each app
 * will create its own instance of this array and the app's IDs
 * will not be unique.
 */
var nextUniqueId = 0;

angular.module('material.core')
  .factory('$mdUtil', function ($cacheFactory, $document, $timeout, $q, $window, $mdConstant,
                                $rootScope) {
    var Util;

    function getNode(el) {
      return el[0] || el;
    }

    return Util = {
      now: window.performance ?
        angular.bind(window.performance, window.performance.now) :
        Date.now,

      clientRect: function (element, offsetParent, isOffsetRect) {
        var node = getNode(element);
        offsetParent = getNode(offsetParent || node.offsetParent || document.body);
        var nodeRect = node.getBoundingClientRect();

        // The user can ask for an offsetRect: a rect relative to the offsetParent,
        // or a clientRect: a rect relative to the page
        var offsetRect = isOffsetRect ?
          offsetParent.getBoundingClientRect() :
        {left: 0, top: 0, width: 0, height: 0};
        return {
          left: nodeRect.left - offsetRect.left,
          top: nodeRect.top - offsetRect.top,
          width: nodeRect.width,
          height: nodeRect.height
        };
      },
      offsetRect: function (element, offsetParent) {
        return Util.clientRect(element, offsetParent, true);
      },

      // Annoying method to copy nodes to an array, thanks to IE
      nodesToArray: function (nodes) {
        nodes = nodes || [ ];

        var results = [];
        for (var i = 0; i < nodes.length; ++i) {
          results.push(nodes.item(i));
        }
        return results;
      },

      // Disables scroll around the passed element.
      disableScrollAround: function (element) {
        Util.disableScrollAround._count = Util.disableScrollAround._count || 0;
        ++Util.disableScrollAround._count;
        if (Util.disableScrollAround._enableScrolling) return Util.disableScrollAround._enableScrolling;
        element = angular.element(element);
        var body = $document[0].body,
          restoreBody = disableBodyScroll(),
          restoreElement = disableElementScroll();

        return Util.disableScrollAround._enableScrolling = function () {
          if (!--Util.disableScrollAround._count) {
            restoreBody();
            restoreElement();
            delete Util.disableScrollAround._enableScrolling;
          }
        };

        // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events
        function disableElementScroll() {
          var zIndex = 50;
          var scrollMask = angular.element(
            '
' +
            '

' +
            '
');
          body.appendChild(scrollMask[0]);

          scrollMask.on('wheel', preventDefault);
          scrollMask.on('touchmove', preventDefault);
          $document.on('keydown', disableKeyNav);

          return function restoreScroll() {
            scrollMask.off('wheel');
            scrollMask.off('touchmove');
            scrollMask[0].parentNode.removeChild(scrollMask[0]);
            $document.off('keydown', disableKeyNav);
            delete Util.disableScrollAround._enableScrolling;
          };

          // Prevent keypresses from elements inside the body
          // used to stop the keypresses that could cause the page to scroll
          // (arrow keys, spacebar, tab, etc).
          function disableKeyNav(e) {
            //-- temporarily removed this logic, will possibly re-add at a later date
            return;
            if (!element[0].contains(e.target)) {
              e.preventDefault();
              e.stopImmediatePropagation();
            }
          }

          function preventDefault(e) {
            e.preventDefault();
          }
        }

        // Converts the body to a position fixed block and translate it to the proper scroll
        // position
        function disableBodyScroll() {
          var htmlNode = body.parentNode;
          var restoreHtmlStyle = htmlNode.getAttribute('style') || '';
          var restoreBodyStyle = body.getAttribute('style') || '';
          var scrollOffset = body.scrollTop + body.parentElement.scrollTop;
          var clientWidth = body.clientWidth;

          if (body.scrollHeight > body.clientHeight) {
            applyStyles(body, {
              position: 'fixed',
              width: '100%',
              top: -scrollOffset + 'px'
            });

            applyStyles(htmlNode, {
              overflowY: 'scroll'
            });
          }

          if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'});

          return function restoreScroll() {
            body.setAttribute('style', restoreBodyStyle);
            htmlNode.setAttribute('style', restoreHtmlStyle);
            body.scrollTop = scrollOffset;
          };
        }

        function applyStyles(el, styles) {
          for (var key in styles) {
            el.style[key] = styles[key];
          }
        }
      },
      enableScrolling: function () {
        var method = this.disableScrollAround._enableScrolling;
        method && method();
      },
      floatingScrollbars: function () {
        if (this.floatingScrollbars.cached === undefined) {
          var tempNode = angular.element('

');
          $document[0].body.appendChild(tempNode[0]);
          this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
          tempNode.remove();
        }
        return this.floatingScrollbars.cached;
      },

      // Mobile safari only allows you to set focus in click event listeners...
      forceFocus: function (element) {
        var node = element[0] || element;

        document.addEventListener('click', function focusOnClick(ev) {
          if (ev.target === node && ev.$focus) {
            node.focus();
            ev.stopImmediatePropagation();
            ev.preventDefault();
            node.removeEventListener('click', focusOnClick);
          }
        }, true);

        var newEvent = document.createEvent('MouseEvents');
        newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
          false, false, false, false, 0, null);
        newEvent.$material = true;
        newEvent.$focus = true;
        node.dispatchEvent(newEvent);
      },

      /**
       * Listen for transitionEnd event (with optional timeout)
       * Announce completion or failure via promise handlers
       */
      transitionEndPromise: function (element, opts) {
        var TIMEOUT = 10000; // fallback is 10 secs

        return $q(function(resolve, reject){
          opts = opts || { };

          var timer = $timeout(finished, opts.timeout || TIMEOUT);
          element.on($mdConstant.CSS.TRANSITIONEND, finished);

          /**
           * Upon timeout or transitionEnd, reject or resolve (respectively) this promise.
           * NOTE: Make sure this transitionEnd didn't bubble up from a child
           */
          function finished(ev) {
            if ( ev && ev.target !== element[0]) return;

            element.off($mdConstant.CSS.TRANSITIONEND, finished);
            if ( ev  ) $timeout.cancel(timer);

            // Only reject if timeout triggered
            var announce = ( ev ) ? resolve : reject;

            announce({
              element:element,
              options:opts
            });
          }

        });
      },

      fakeNgModel: function () {
        return {
          $fake: true,
          $setTouched: angular.noop,
          $setViewValue: function (value) {
            this.$viewValue = value;
            this.$render(value);
            this.$viewChangeListeners.forEach(function (cb) {
              cb();
            });
          },
          $isEmpty: function (value) {
            return ('' + value).length === 0;
          },
          $parsers: [],
          $formatters: [],
          $viewChangeListeners: [],
          $render: angular.noop
        };
      },

      // Returns a function, that, as long as it continues to be invoked, will not
      // be triggered. The function will be called after it stops being called for
      // N milliseconds.
      // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
      // @param invokeApply should the $timeout trigger $digest() dirty checking
      debounce: function (func, wait, scope, invokeApply) {
        var timer;

        return function debounced() {
          var context = scope,
            args = Array.prototype.slice.call(arguments);

          $timeout.cancel(timer);
          timer = $timeout(function () {

            timer = undefined;
            func.apply(context, args);

          }, wait || 10, invokeApply);
        };
      },

      // Returns a function that can only be triggered every `delay` milliseconds.
      // In other words, the function will not be called unless it has been more
      // than `delay` milliseconds since the last call.
      throttle: function throttle(func, delay) {
        var recent;
        return function throttled() {
          var context = this;
          var args = arguments;
          var now = Util.now();

          if (!recent || (now - recent > delay)) {
            func.apply(context, args);
            recent = now;
          }
        };
      },

      /**
       * Measures the number of milliseconds taken to run the provided callback
       * function. Uses a high-precision timer if available.
       */
      time: function time(cb) {
        var start = Util.now();
        cb();
        return Util.now() - start;
      },

      /**
       * Get a unique ID.
       *
       * @returns {string} an unique numeric string
       */
      nextUid: function () {
        return '' + nextUniqueId++;
      },

      // Stop watchers and events from firing on a scope without destroying it,
      // by disconnecting it from its parent and its siblings' linked lists.
      disconnectScope: function disconnectScope(scope) {
        if (!scope) return;

        // we can't destroy the root scope or a scope that has been already destroyed
        if (scope.$root === scope) return;
        if (scope.$$destroyed) return;

        var parent = scope.$parent;
        scope.$$disconnected = true;

        // See Scope.$destroy
        if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
        if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
        if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
        if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;

        scope.$$nextSibling = scope.$$prevSibling = null;

      },

      // Undo the effects of disconnectScope above.
      reconnectScope: function reconnectScope(scope) {
        if (!scope) return;

        // we can't disconnect the root node or scope already disconnected
        if (scope.$root === scope) return;
        if (!scope.$$disconnected) return;

        var child = scope;

        var parent = child.$parent;
        child.$$disconnected = false;
        // See Scope.$new for this logic...
        child.$$prevSibling = parent.$$childTail;
        if (parent.$$childHead) {
          parent.$$childTail.$$nextSibling = child;
          parent.$$childTail = child;
        } else {
          parent.$$childHead = parent.$$childTail = child;
        }
      },

      /*
       * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName
       *
       * @param el Element to start walking the DOM from
       * @param tagName Tag name to find closest to el, such as 'form'
       */
      getClosest: function getClosest(el, tagName, onlyParent) {
        if (el instanceof angular.element) el = el[0];
        tagName = tagName.toUpperCase();
        if (onlyParent) el = el.parentNode;
        if (!el) return null;
        do {
          if (el.nodeName === tagName) {
            return el;
          }
        } while (el = el.parentNode);
        return null;
      },

      /**
       * Functional equivalent for $element.filter(‘md-bottom-sheet’)
       * useful with interimElements where the element and its container are important...
       */
      extractElementByName: function (element, nodeName) {
        for (var i = 0, len = element.length; i < len; i++) {
          if (element[i].nodeName.toLowerCase() === nodeName) {
            return angular.element(element[i]);
          }
        }
        return element;
      },

      /**
       * Give optional properties with no value a boolean true if attr provided or false otherwise
       */
      initOptionalProperties: function (scope, attr, defaults) {
        defaults = defaults || {};
        angular.forEach(scope.$$isolateBindings, function (binding, key) {
          if (binding.optional && angular.isUndefined(scope[key])) {
            var attrIsDefined = angular.isDefined(attr[binding.attrName]);
            scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined;
          }
        });
      },

      nextTick: function (callback, digest) {
        //-- grab function reference for storing state details
        var nextTick = this.nextTick;
        var timeout = nextTick.timeout;
        var queue = nextTick.queue || [];

        //-- set default value for digest to true
        if (digest == null) digest = true;

        //-- store updated digest value
        nextTick.digest = nextTick.digest || digest;

        //-- update queue/digest values
        queue = nextTick.queue || [];
        queue.push(callback);

        //-- set timeout flag to prevent other timeouts from being created until this is finished
        nextTick.timeout = true;

        //-- store the queue
        nextTick.queue = queue;

        //-- return either the existing timeout or the newly created one
        return timeout || (nextTick.timeout = $timeout(processQueue, 0, false));

        function processQueue () {
          //-- grab a copy of the current queue
          var queue = nextTick.queue;
          var digest = nextTick.digest;

          //-- reset the queue just in case any callbacks use nextTick
          nextTick.queue = [];
          nextTick.callback = false;
          nextTick.timeout = false;
          nextTick.digest = false;

          //-- process the existing queue
          queue.forEach(function (callback) { callback(); });

          //-- trigger digest if necessary
          if (digest) $rootScope.$digest();
        }
      }
    };

  });

/*
 * Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
 *
 * We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
 *
 * TODO(ajoslin): This should be added in a better place later.
 */

angular.element.prototype.focus = angular.element.prototype.focus || function () {
    if (this.length) {
      this[0].focus();
    }
    return this;
  };
angular.element.prototype.blur = angular.element.prototype.blur || function () {
    if (this.length) {
      this[0].blur();
    }
    return this;
  };
<commit_msg>update(util): replaces `this` with `Util` in `nextTick`<commit_after>/*
 * This var has to be outside the angular factory, otherwise when
 * there are multiple material apps on the same page, each app
 * will create its own instance of this array and the app's IDs
 * will not be unique.
 */
var nextUniqueId = 0;

angular.module('material.core')
  .factory('$mdUtil', function ($cacheFactory, $document, $timeout, $q, $window, $mdConstant,
                                $rootScope) {
    var Util;

    function getNode(el) {
      return el[0] || el;
    }

    return Util = {
      now: window.performance ?
        angular.bind(window.performance, window.performance.now) :
        Date.now,

      clientRect: function (element, offsetParent, isOffsetRect) {
        var node = getNode(element);
        offsetParent = getNode(offsetParent || node.offsetParent || document.body);
        var nodeRect = node.getBoundingClientRect();

        // The user can ask for an offsetRect: a rect relative to the offsetParent,
        // or a clientRect: a rect relative to the page
        var offsetRect = isOffsetRect ?
          offsetParent.getBoundingClientRect() :
        {left: 0, top: 0, width: 0, height: 0};
        return {
          left: nodeRect.left - offsetRect.left,
          top: nodeRect.top - offsetRect.top,
          width: nodeRect.width,
          height: nodeRect.height
        };
      },
      offsetRect: function (element, offsetParent) {
        return Util.clientRect(element, offsetParent, true);
      },

      // Annoying method to copy nodes to an array, thanks to IE
      nodesToArray: function (nodes) {
        nodes = nodes || [ ];

        var results = [];
        for (var i = 0; i < nodes.length; ++i) {
          results.push(nodes.item(i));
        }
        return results;
      },

      // Disables scroll around the passed element.
      disableScrollAround: function (element) {
        Util.disableScrollAround._count = Util.disableScrollAround._count || 0;
        ++Util.disableScrollAround._count;
        if (Util.disableScrollAround._enableScrolling) return Util.disableScrollAround._enableScrolling;
        element = angular.element(element);
        var body = $document[0].body,
          restoreBody = disableBodyScroll(),
          restoreElement = disableElementScroll();

        return Util.disableScrollAround._enableScrolling = function () {
          if (!--Util.disableScrollAround._count) {
            restoreBody();
            restoreElement();
            delete Util.disableScrollAround._enableScrolling;
          }
        };

        // Creates a virtual scrolling mask to absorb touchmove, keyboard, scrollbar clicking, and wheel events
        function disableElementScroll() {
          var zIndex = 50;
          var scrollMask = angular.element(
            '
' +
            '

' +
            '
');
          body.appendChild(scrollMask[0]);

          scrollMask.on('wheel', preventDefault);
          scrollMask.on('touchmove', preventDefault);
          $document.on('keydown', disableKeyNav);

          return function restoreScroll() {
            scrollMask.off('wheel');
            scrollMask.off('touchmove');
            scrollMask[0].parentNode.removeChild(scrollMask[0]);
            $document.off('keydown', disableKeyNav);
            delete Util.disableScrollAround._enableScrolling;
          };

          // Prevent keypresses from elements inside the body
          // used to stop the keypresses that could cause the page to scroll
          // (arrow keys, spacebar, tab, etc).
          function disableKeyNav(e) {
            //-- temporarily removed this logic, will possibly re-add at a later date
            return;
            if (!element[0].contains(e.target)) {
              e.preventDefault();
              e.stopImmediatePropagation();
            }
          }

          function preventDefault(e) {
            e.preventDefault();
          }
        }

        // Converts the body to a position fixed block and translate it to the proper scroll
        // position
        function disableBodyScroll() {
          var htmlNode = body.parentNode;
          var restoreHtmlStyle = htmlNode.getAttribute('style') || '';
          var restoreBodyStyle = body.getAttribute('style') || '';
          var scrollOffset = body.scrollTop + body.parentElement.scrollTop;
          var clientWidth = body.clientWidth;

          if (body.scrollHeight > body.clientHeight) {
            applyStyles(body, {
              position: 'fixed',
              width: '100%',
              top: -scrollOffset + 'px'
            });

            applyStyles(htmlNode, {
              overflowY: 'scroll'
            });
          }

          if (body.clientWidth < clientWidth) applyStyles(body, {overflow: 'hidden'});

          return function restoreScroll() {
            body.setAttribute('style', restoreBodyStyle);
            htmlNode.setAttribute('style', restoreHtmlStyle);
            body.scrollTop = scrollOffset;
          };
        }

        function applyStyles(el, styles) {
          for (var key in styles) {
            el.style[key] = styles[key];
          }
        }
      },
      enableScrolling: function () {
        var method = this.disableScrollAround._enableScrolling;
        method && method();
      },
      floatingScrollbars: function () {
        if (this.floatingScrollbars.cached === undefined) {
          var tempNode = angular.element('

');
          $document[0].body.appendChild(tempNode[0]);
          this.floatingScrollbars.cached = (tempNode[0].offsetWidth == tempNode[0].childNodes[0].offsetWidth);
          tempNode.remove();
        }
        return this.floatingScrollbars.cached;
      },

      // Mobile safari only allows you to set focus in click event listeners...
      forceFocus: function (element) {
        var node = element[0] || element;

        document.addEventListener('click', function focusOnClick(ev) {
          if (ev.target === node && ev.$focus) {
            node.focus();
            ev.stopImmediatePropagation();
            ev.preventDefault();
            node.removeEventListener('click', focusOnClick);
          }
        }, true);

        var newEvent = document.createEvent('MouseEvents');
        newEvent.initMouseEvent('click', false, true, window, {}, 0, 0, 0, 0,
          false, false, false, false, 0, null);
        newEvent.$material = true;
        newEvent.$focus = true;
        node.dispatchEvent(newEvent);
      },

      /**
       * Listen for transitionEnd event (with optional timeout)
       * Announce completion or failure via promise handlers
       */
      transitionEndPromise: function (element, opts) {
        var TIMEOUT = 10000; // fallback is 10 secs

        return $q(function(resolve, reject){
          opts = opts || { };

          var timer = $timeout(finished, opts.timeout || TIMEOUT);
          element.on($mdConstant.CSS.TRANSITIONEND, finished);

          /**
           * Upon timeout or transitionEnd, reject or resolve (respectively) this promise.
           * NOTE: Make sure this transitionEnd didn't bubble up from a child
           */
          function finished(ev) {
            if ( ev && ev.target !== element[0]) return;

            element.off($mdConstant.CSS.TRANSITIONEND, finished);
            if ( ev  ) $timeout.cancel(timer);

            // Only reject if timeout triggered
            var announce = ( ev ) ? resolve : reject;

            announce({
              element:element,
              options:opts
            });
          }

        });
      },

      fakeNgModel: function () {
        return {
          $fake: true,
          $setTouched: angular.noop,
          $setViewValue: function (value) {
            this.$viewValue = value;
            this.$render(value);
            this.$viewChangeListeners.forEach(function (cb) {
              cb();
            });
          },
          $isEmpty: function (value) {
            return ('' + value).length === 0;
          },
          $parsers: [],
          $formatters: [],
          $viewChangeListeners: [],
          $render: angular.noop
        };
      },

      // Returns a function, that, as long as it continues to be invoked, will not
      // be triggered. The function will be called after it stops being called for
      // N milliseconds.
      // @param wait Integer value of msecs to delay (since last debounce reset); default value 10 msecs
      // @param invokeApply should the $timeout trigger $digest() dirty checking
      debounce: function (func, wait, scope, invokeApply) {
        var timer;

        return function debounced() {
          var context = scope,
            args = Array.prototype.slice.call(arguments);

          $timeout.cancel(timer);
          timer = $timeout(function () {

            timer = undefined;
            func.apply(context, args);

          }, wait || 10, invokeApply);
        };
      },

      // Returns a function that can only be triggered every `delay` milliseconds.
      // In other words, the function will not be called unless it has been more
      // than `delay` milliseconds since the last call.
      throttle: function throttle(func, delay) {
        var recent;
        return function throttled() {
          var context = this;
          var args = arguments;
          var now = Util.now();

          if (!recent || (now - recent > delay)) {
            func.apply(context, args);
            recent = now;
          }
        };
      },

      /**
       * Measures the number of milliseconds taken to run the provided callback
       * function. Uses a high-precision timer if available.
       */
      time: function time(cb) {
        var start = Util.now();
        cb();
        return Util.now() - start;
      },

      /**
       * Get a unique ID.
       *
       * @returns {string} an unique numeric string
       */
      nextUid: function () {
        return '' + nextUniqueId++;
      },

      // Stop watchers and events from firing on a scope without destroying it,
      // by disconnecting it from its parent and its siblings' linked lists.
      disconnectScope: function disconnectScope(scope) {
        if (!scope) return;

        // we can't destroy the root scope or a scope that has been already destroyed
        if (scope.$root === scope) return;
        if (scope.$$destroyed) return;

        var parent = scope.$parent;
        scope.$$disconnected = true;

        // See Scope.$destroy
        if (parent.$$childHead === scope) parent.$$childHead = scope.$$nextSibling;
        if (parent.$$childTail === scope) parent.$$childTail = scope.$$prevSibling;
        if (scope.$$prevSibling) scope.$$prevSibling.$$nextSibling = scope.$$nextSibling;
        if (scope.$$nextSibling) scope.$$nextSibling.$$prevSibling = scope.$$prevSibling;

        scope.$$nextSibling = scope.$$prevSibling = null;

      },

      // Undo the effects of disconnectScope above.
      reconnectScope: function reconnectScope(scope) {
        if (!scope) return;

        // we can't disconnect the root node or scope already disconnected
        if (scope.$root === scope) return;
        if (!scope.$$disconnected) return;

        var child = scope;

        var parent = child.$parent;
        child.$$disconnected = false;
        // See Scope.$new for this logic...
        child.$$prevSibling = parent.$$childTail;
        if (parent.$$childHead) {
          parent.$$childTail.$$nextSibling = child;
          parent.$$childTail = child;
        } else {
          parent.$$childHead = parent.$$childTail = child;
        }
      },

      /*
       * getClosest replicates jQuery.closest() to walk up the DOM tree until it finds a matching nodeName
       *
       * @param el Element to start walking the DOM from
       * @param tagName Tag name to find closest to el, such as 'form'
       */
      getClosest: function getClosest(el, tagName, onlyParent) {
        if (el instanceof angular.element) el = el[0];
        tagName = tagName.toUpperCase();
        if (onlyParent) el = el.parentNode;
        if (!el) return null;
        do {
          if (el.nodeName === tagName) {
            return el;
          }
        } while (el = el.parentNode);
        return null;
      },

      /**
       * Functional equivalent for $element.filter(‘md-bottom-sheet’)
       * useful with interimElements where the element and its container are important...
       */
      extractElementByName: function (element, nodeName) {
        for (var i = 0, len = element.length; i < len; i++) {
          if (element[i].nodeName.toLowerCase() === nodeName) {
            return angular.element(element[i]);
          }
        }
        return element;
      },

      /**
       * Give optional properties with no value a boolean true if attr provided or false otherwise
       */
      initOptionalProperties: function (scope, attr, defaults) {
        defaults = defaults || {};
        angular.forEach(scope.$$isolateBindings, function (binding, key) {
          if (binding.optional && angular.isUndefined(scope[key])) {
            var attrIsDefined = angular.isDefined(attr[binding.attrName]);
            scope[key] = angular.isDefined(defaults[key]) ? defaults[key] : attrIsDefined;
          }
        });
      },

      nextTick: function (callback, digest) {
        //-- grab function reference for storing state details
        var nextTick = Util.nextTick;
        var timeout = nextTick.timeout;
        var queue = nextTick.queue || [];

        //-- set default value for digest to true
        if (digest == null) digest = true;

        //-- store updated digest value
        nextTick.digest = nextTick.digest || digest;

        //-- update queue/digest values
        queue = nextTick.queue || [];
        queue.push(callback);

        //-- set timeout flag to prevent other timeouts from being created until this is finished
        nextTick.timeout = true;

        //-- store the queue
        nextTick.queue = queue;

        //-- return either the existing timeout or the newly created one
        return timeout || (nextTick.timeout = $timeout(processQueue, 0, false));

        function processQueue () {
          //-- grab a copy of the current queue
          var queue = nextTick.queue;
          var digest = nextTick.digest;

          //-- reset the queue just in case any callbacks use nextTick
          nextTick.queue = [];
          nextTick.callback = false;
          nextTick.timeout = false;
          nextTick.digest = false;

          //-- process the existing queue
          queue.forEach(function (callback) { callback(); });

          //-- trigger digest if necessary
          if (digest) $rootScope.$digest();
        }
      }
    };

  });

/*
 * Since removing jQuery from the demos, some code that uses `element.focus()` is broken.
 *
 * We need to add `element.focus()`, because it's testable unlike `element[0].focus`.
 *
 * TODO(ajoslin): This should be added in a better place later.
 */

angular.element.prototype.focus = angular.element.prototype.focus || function () {
    if (this.length) {
      this[0].focus();
    }
    return this;
  };
angular.element.prototype.blur = angular.element.prototype.blur || function () {
    if (this.length) {
      this[0].blur();
    }
    return this;
  };
<|endoftext|>


<commit_before>function updatetab(url, pane) {
// http://docs.mathjax.org/en/latest/tex.html
    // From http://stackoverflow.com/a/651735
    var ext = url.split('.').pop().toLowerCase();
    var ismd = true;
    if ($.inArray(ext, ['md', 'markdown', 'mdown', 'mkdn', 'mkd', 'mdtxt', 'mdtext']) == -1) {
      ismd = false;
    }
    $.get(url, function(data) {
        if (ismd) {

          data= data.replace(/\_/gi,'\\\_');// Handle underscore by \_
          $('#my-pagination-content').html(marked(data));
          MathJax.Hub.Queue(["Typeset", MathJax.Hub, "my-pagination-content"]);
        } else {
          $('#my-pagination-content').html(data);
          MathJax.Hub.Queue(["Typeset", MathJax.Hub, "my-pagination-content"]);
        }
        console.log('makerbutton');
        makerbutton(); //Update buttons.
        pane.tab('show');
    });
  }

<commit_msg>Update updatetab.js<commit_after>function updatetab(url, pane) {
// http://docs.mathjax.org/en/latest/tex.html
    // From http://stackoverflow.com/a/651735
    var ext = url.split('.').pop().toLowerCase();
    var ismd = true;
    if ($.inArray(ext, ['md', 'markdown', 'mdown', 'mkdn', 'mkd', 'mdtxt', 'mdtext']) == -1) {
      ismd = false;
    }
    $.get(url, function(data) {
        if (ismd) {

          data= data.replace(/(?<=\\[)(\_)(?=\\])/gi,'\\\_'); // Handle underscore by \_
          // only change if _ is within math symbols http://stackoverflow.com/a/1454936
          $('#my-pagination-content').html(marked(data));
          MathJax.Hub.Queue(["Typeset", MathJax.Hub, "my-pagination-content"]);
        } else {
          $('#my-pagination-content').html(data);
          MathJax.Hub.Queue(["Typeset", MathJax.Hub, "my-pagination-content"]);
        }
        console.log('makerbutton');
        makerbutton(); //Update buttons.
        pane.tab('show');
    });
  }

<|endoftext|>


<commit_before>Behaviour.register({
	'#userName' : function(el) {
		el.focus();
		el.select();
	}
});<commit_msg>Focus different fields in add / edit modes.<commit_after>Behaviour.register({
	'#emailAddress' : function(el) {
		el.focus();
		el.select();
	},
	'#userName' : function(el) {
		el.focus();
		el.select();
	}
});<|endoftext|>


<commit_before>/**
 * Created by Amar on 21.11.2016.
 */

"use strict";

class SceneInit {

    constructor(domtarget, sceneOptions, camera, scene, controls, renderer, mouse, INTERSECTED) {
        this.domtarget = domtarget;
        this.sceneOptions = sceneOptions; //custom user options held here
        this.camera = camera;
        this.scene = scene;
        this.controls = controls;
        this.renderer = renderer;
        this.mouse = new THREE.Vector2();
        this.INTERSECTED = INTERSECTED;
    }

    initScene() {
        this.camera = new THREE.PerspectiveCamera(this.fov, window.innerWidth / window.innerHeight, 1, 1000);

        if(this.sceneOptions.startAnimation) {
            this.camera.position.set(0, -10, 1100);
            this.entryAnimation();
        }else{
            this.camera.position.set(0, -10, 7);
        }

        this.controls = new THREE.OrbitControls(this.camera);
        this.controls.addEventListener('change', this.render.bind(this));

        this.scene = new THREE.Scene();

        //specify a canvas which is already created in the HTML file and tagged by an id
        this.renderer = new THREE.WebGLRenderer({canvas: document.getElementById(this.domtarget),
            antialias: this.sceneOptions.antialias, alpha: this.sceneOptions.transparency });

        this.renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(this.renderer.domElement);

        if(this.sceneOptions.bgcolor){
            this.scene.background = new THREE.Color(this.sceneOptions.bgcolor);
        }

        //ambient light which is for the whole scene
        let ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
        ambientLight.castShadow = false;
        this.scene.add(ambientLight);

        //spot light which is illuminating the chart directly
        let spotLight = new THREE.SpotLight(0xffffff, 0.55);
        spotLight.castShadow = true;
        spotLight.position.set(0, 40, 10);
        this.scene.add(spotLight);

        document.addEventListener('mousedown', this.onDocumentMouseAction.bind(this), false);
        document.addEventListener('mousemove', this.onDocumentMouseAction.bind(this), false);
        document.addEventListener('keydown', this.onDocumentKeyAction.bind(this),false);
        document.ondblclick = this.onDocumentDblClick.bind(this);

        //if window resizes
        window.addEventListener('resize', this.onWindowResize.bind(this), false);
    }

    animate() {
        requestAnimationFrame(this.animate.bind(this));
        this.render();
        this.controls.update();
    }

    render() {
        this.renderer.render(this.scene, this.camera);
        TWEEN.update();
    }

    onWindowResize() {
        this.camera.aspect = window.innerWidth / window.innerHeight;
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(window.innerWidth, window.innerHeight);
    }

    findIntersections(event) {
        let raycaster = new THREE.Raycaster();
        this.mouse.x = ( event.clientX / this.renderer.domElement.clientWidth ) * 2 - 1;
        this.mouse.y = -( event.clientY / this.renderer.domElement.clientHeight ) * 2 + 1;
        raycaster.setFromCamera(this.mouse, this.camera);

        //search for our object by name which we declared before and return it
        return raycaster.intersectObjects(this.scene.getObjectByName("groupedChart", true).children);
    }

    onDocumentKeyAction(event) {
        switch (event.keyCode) {
            case 37: //left arrow
                // this.scene.getObjectByName("groupedPieChart", true).rotation.z += 0.1;
                break;
            case 38: //up arrow
                // this.scene.getObjectByName("groupedPieChart", true).rotation.x += 0.1;
                break;
            case 39: //right arrow
                // this.scene.getObjectByName("groupedPieChart", true).rotation.z -= 0.1;
                break;
            case 40: //down arrow
                // this.scene.getObjectByName("groupedPieChart", true).rotation.x -= 0.1;
                break;
            case 82: //R button
                this.resetCameraPosition();
                break;
            case 67:
                this.showOnScreenControls();
                break;
        }
    }

    showOnScreenControls() {
        document.getElementById("controls").innerHTML = `<button id="resetBtn">Reset</button>`;
        document.querySelector("#resetBtn").addEventListener('click',this.resetCameraPosition.bind(this),false);
    }

    onDocumentMouseAction(event) {
        //call function which finds intersected objects
        let intersects = this.findIntersections(event);

        if (intersects[0] !== undefined && event.type === "mousedown") {//if the event type is a mouse click (one click)
            //print percentage of the clicked section + the name of the object assigned in the 'create3DPieChart' function
            //intersects[0] because we want the first intersected object and every other object which may lies in the background is unnecessary
            document.getElementById("details").innerHTML = "
" + intersects[0].object.name + "
" + intersects[0].object.data1.name + ": " + intersects[0].object.data1.percent.toFixed(2) +
                "% (" + intersects[0].object.data1.value + ")" + "
" + intersects[0].object.data2.name + ": " + intersects[0].object.data2.percent.toFixed(2) + "% (" + intersects[0].object.data2.value + ")";
        }
        else if (intersects[0] !== undefined && event.type == "mousemove") {//if the event type is a mouse move (hover)

            //call the html tooltip whenever there is an intersected object. if it is the same call again to update position
            if (this.INTERSECTED) {
                this.htmlTooltip("show");
            }

            if (this.INTERSECTED != intersects[0].object) {
                if (this.INTERSECTED) this.INTERSECTED.material.emissive.setHex(this.INTERSECTED.currentHex);
                this.INTERSECTED = intersects[0].object;
                this.currentHex = this.INTERSECTED.material.emissive.getHex();
                this.INTERSECTED.material.emissive.setHex(0xa9a8a8);
            }
        }
        else {
            if (this.INTERSECTED) this.INTERSECTED.material.emissive.setHex(this.INTERSECTED.currentHex);
            this.INTERSECTED = null;

            this.htmlTooltip("hide");
        }

    }

    onDocumentDblClick() {
        //IMPLEMENT DOUBLE CLICK FUNCTION HERE
    }

    htmlTooltip(status) {

        let tooltip = null;
        status = (!status) ? "show" : status;

        if (status === "show" && this.sceneOptions.tooltip) {

            if (!document.getElementById("tooltip")) {
                tooltip = document.createElement("div");
            } else {
                tooltip = document.getElementById("tooltip");
            }

            tooltip.setAttribute("id", "tooltip");
            tooltip.innerHTML =
                `
${this.INTERSECTED.name}

                     ${this.INTERSECTED.data1.name}: ${this.INTERSECTED.data1.value} (${this.INTERSECTED.data1.percent.toFixed(2)}%)

                     ${this.INTERSECTED.data2.name}: ${this.INTERSECTED.data2.value} (${this.INTERSECTED.data2.percent.toFixed(2)}%)`;

            let vector = new THREE.Vector3(this.mouse.x, this.mouse.y);
            tooltip.style.position = "absolute";

            let posX = 0;
            let posY = 0;
            let q = "";

            if (vector.x >= 0 && vector.y >= 0) {
                q = "q1"
            }
            if (vector.x <= 0 && vector.y >= 0) {
                q = "q2"
            }
            if (vector.x <= 0 && vector.y <= 0) {
                q = "q3"
            }
            if (vector.x >= 0 && vector.y <= 0) {
                q = "q4"
            }

            if (q == "q1") {
                posX = ((Math.abs(vector.x)) * 50) + 50;
                posY = ((1 - Math.abs(vector.y)) * 50);
            }

            if (q == "q2") {
                posX = ((1 - Math.abs(vector.x)) * 50);
                posY = ((1 - Math.abs(vector.y)) * 50);
            }

            if (q == "q3") {
                posX = ((1 - Math.abs(vector.x)) * 50);
                posY = ((Math.abs(vector.y)) * 50) + 50;
            }

            if (q == "q4") {
                posX = ((Math.abs(vector.x)) * 50) + 50;
                posY = ((Math.abs(vector.y)) * 50) + 50;
            }

            tooltip.style.left = posX + '%';
            tooltip.style.top = posY + '%';

            document.body.appendChild(tooltip);
        }
        //remove tooltip if mouse does not hover over the pie or a pie segment
        else if (status === "hide" && document.getElementById("tooltip")) {
            document.body.removeChild(document.getElementById("tooltip"));
        }
    }

    entryAnimation() {
        let cam = this.camera;
        let startPos = {x: cam.position.x, y: cam.position.y, z: cam.position.z};
        let endPos = {x: 0, y: -10, z: 7};

        new TWEEN.Tween(startPos)
            .to({x: endPos.x, y: endPos.y, z: endPos.z}, 2500)
            .easing(TWEEN.Easing.Cubic.Out)
            .onUpdate(function () {
                cam.position.setY(startPos.y);
                cam.position.setZ(startPos.z);
            })
            .delay(800)
            .start();

    }

    resetCameraPosition(){
        let cam = this.camera;
        let actualPos = {x: cam.position.x, y: cam.position.y, z: Math.ceil(cam.position.z)}; //ceiling upwards cause of minimal variety
        let defaultPos = {x: 0, y: -10, z: 7};
        let initPos = (actualPos.x == defaultPos.x && actualPos.y == defaultPos.y && actualPos.z == defaultPos.z);

        if(!initPos) {
            new TWEEN.Tween(actualPos)
                .to({x: defaultPos.x, y: defaultPos.y, z: defaultPos.z}, 4000)
                .easing(TWEEN.Easing.Cubic.Out)
                .onUpdate(function () {
                    cam.position.set(actualPos.x,actualPos.y,actualPos.z);
                }).start();
        }
    }

    /*resetObjectPosition(){
        let object = this.scene.getObjectByName("groupedPieChart", true);
        let actualPos = {x: object.rotation.x, y: object.rotation.y, z: object.rotation.z};
        let defaultPos = {x: 0, y: 0, z: 0};

        let initPos = (actualPos.x == defaultPos.x && actualPos.y == defaultPos.y && actualPos.z == defaultPos.z);

        if(!initPos) {
            new TWEEN.Tween(actualPos)
                .to({x: defaultPos.x, y: defaultPos.y, z: defaultPos.z}, 4000)
                .easing(TWEEN.Easing.Cubic.Out)
                .onUpdate(function () {
                    object.rotation.set(actualPos.x,actualPos.y,actualPos.z);
                }).start();
        }
    }*/

}

export {SceneInit}
<commit_msg>modified a merge error<commit_after>/**
 * Created by Amar on 21.11.2016.
 */

"use strict";

class SceneInit {

    constructor(domtarget, sceneOptions, camera, scene, controls, renderer, mouse, INTERSECTED) {
        this.domtarget = domtarget;
        this.sceneOptions = sceneOptions; //custom user options held here
        this.camera = camera;
        this.scene = scene;
        this.controls = controls;
        this.renderer = renderer;
        this.mouse = new THREE.Vector2();
        this.INTERSECTED = INTERSECTED;
    }

    initScene() {
        this.camera = new THREE.PerspectiveCamera(this.sceneOptions.fov, window.innerWidth / window.innerHeight, 1, 1000);

        if(this.sceneOptions.startAnimation) {
            this.camera.position.set(0, -10, 1100);
            this.entryAnimation();
        }else{
            this.camera.position.set(0, -10, 7);
        }

        this.controls = new THREE.OrbitControls(this.camera);
        this.controls.addEventListener('change', this.render.bind(this));

        this.scene = new THREE.Scene();

        //specify a canvas which is already created in the HTML file and tagged by an id
        this.renderer = new THREE.WebGLRenderer({canvas: document.getElementById(this.domtarget),
            antialias: this.sceneOptions.antialias, alpha: this.sceneOptions.transparency });

        this.renderer.setSize(window.innerWidth, window.innerHeight);
        document.body.appendChild(this.renderer.domElement);

        if(this.sceneOptions.bgcolor){
            this.scene.background = new THREE.Color(this.sceneOptions.bgcolor);
        }

        //ambient light which is for the whole scene
        let ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
        ambientLight.castShadow = false;
        this.scene.add(ambientLight);

        //spot light which is illuminating the chart directly
        let spotLight = new THREE.SpotLight(0xffffff, 0.55);
        spotLight.castShadow = true;
        spotLight.position.set(0, 40, 10);
        this.scene.add(spotLight);

        document.addEventListener('mousedown', this.onDocumentMouseAction.bind(this), false);
        document.addEventListener('mousemove', this.onDocumentMouseAction.bind(this), false);
        document.addEventListener('keydown', this.onDocumentKeyAction.bind(this),false);
        document.ondblclick = this.onDocumentDblClick.bind(this);

        //if window resizes
        window.addEventListener('resize', this.onWindowResize.bind(this), false);
    }

    animate() {
        requestAnimationFrame(this.animate.bind(this));
        this.render();
        this.controls.update();
    }

    render() {
        this.renderer.render(this.scene, this.camera);
        TWEEN.update();
    }

    onWindowResize() {
        this.camera.aspect = window.innerWidth / window.innerHeight;
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(window.innerWidth, window.innerHeight);
    }

    findIntersections(event) {
        let raycaster = new THREE.Raycaster();
        this.mouse.x = ( event.clientX / this.renderer.domElement.clientWidth ) * 2 - 1;
        this.mouse.y = -( event.clientY / this.renderer.domElement.clientHeight ) * 2 + 1;
        raycaster.setFromCamera(this.mouse, this.camera);

        //search for our object by name which we declared before and return it
        return raycaster.intersectObjects(this.scene.getObjectByName("groupedChart", true).children);
    }

    onDocumentKeyAction(event) {
        switch (event.keyCode) {
            case 37: //left arrow
                // this.scene.getObjectByName("groupedPieChart", true).rotation.z += 0.1;
                break;
            case 38: //up arrow
                // this.scene.getObjectByName("groupedPieChart", true).rotation.x += 0.1;
                break;
            case 39: //right arrow
                // this.scene.getObjectByName("groupedPieChart", true).rotation.z -= 0.1;
                break;
            case 40: //down arrow
                // this.scene.getObjectByName("groupedPieChart", true).rotation.x -= 0.1;
                break;
            case 82: //R button
                this.resetCameraPosition();
                break;
            case 67:
                this.showOnScreenControls();
                break;
        }
    }

    showOnScreenControls() {
        document.getElementById("controls").innerHTML = `<button id="resetBtn">Reset</button>`;
        document.querySelector("#resetBtn").addEventListener('click',this.resetCameraPosition.bind(this),false);
    }

    onDocumentMouseAction(event) {
        //call function which finds intersected objects
        let intersects = this.findIntersections(event);

        if (intersects[0] !== undefined && event.type === "mousedown") {//if the event type is a mouse click (one click)
            //print percentage of the clicked section + the name of the object assigned in the 'create3DPieChart' function
            //intersects[0] because we want the first intersected object and every other object which may lies in the background is unnecessary
            document.getElementById("details").innerHTML = "
" + intersects[0].object.name + "
" + intersects[0].object.data1.name + ": " + intersects[0].object.data1.percent.toFixed(2) +
                "% (" + intersects[0].object.data1.value + ")" + "
" + intersects[0].object.data2.name + ": " + intersects[0].object.data2.percent.toFixed(2) + "% (" + intersects[0].object.data2.value + ")";
        }
        else if (intersects[0] !== undefined && event.type == "mousemove") {//if the event type is a mouse move (hover)

            //call the html tooltip whenever there is an intersected object. if it is the same call again to update position
            if (this.INTERSECTED) {
                this.htmlTooltip("show");
            }

            if (this.INTERSECTED != intersects[0].object) {
                if (this.INTERSECTED) this.INTERSECTED.material.emissive.setHex(this.INTERSECTED.currentHex);
                this.INTERSECTED = intersects[0].object;
                this.currentHex = this.INTERSECTED.material.emissive.getHex();
                this.INTERSECTED.material.emissive.setHex(0xa9a8a8);
            }
        }
        else {
            if (this.INTERSECTED) this.INTERSECTED.material.emissive.setHex(this.INTERSECTED.currentHex);
            this.INTERSECTED = null;

            this.htmlTooltip("hide");
        }

    }

    onDocumentDblClick() {
        //IMPLEMENT DOUBLE CLICK FUNCTION HERE
    }

    htmlTooltip(status) {

        let tooltip = null;
        status = (!status) ? "show" : status;

        if (status === "show" && this.sceneOptions.tooltip) {

            if (!document.getElementById("tooltip")) {
                tooltip = document.createElement("div");
            } else {
                tooltip = document.getElementById("tooltip");
            }

            tooltip.setAttribute("id", "tooltip");
            tooltip.innerHTML =
                `
${this.INTERSECTED.name}

                     ${this.INTERSECTED.data1.name}: ${this.INTERSECTED.data1.value} (${this.INTERSECTED.data1.percent.toFixed(2)}%)

                     ${this.INTERSECTED.data2.name}: ${this.INTERSECTED.data2.value} (${this.INTERSECTED.data2.percent.toFixed(2)}%)`;

            let vector = new THREE.Vector3(this.mouse.x, this.mouse.y);
            tooltip.style.position = "absolute";

            let posX = 0;
            let posY = 0;
            let q = "";

            if (vector.x >= 0 && vector.y >= 0) {
                q = "q1"
            }
            if (vector.x <= 0 && vector.y >= 0) {
                q = "q2"
            }
            if (vector.x <= 0 && vector.y <= 0) {
                q = "q3"
            }
            if (vector.x >= 0 && vector.y <= 0) {
                q = "q4"
            }

            if (q == "q1") {
                posX = ((Math.abs(vector.x)) * 50) + 50;
                posY = ((1 - Math.abs(vector.y)) * 50);
            }

            if (q == "q2") {
                posX = ((1 - Math.abs(vector.x)) * 50);
                posY = ((1 - Math.abs(vector.y)) * 50);
            }

            if (q == "q3") {
                posX = ((1 - Math.abs(vector.x)) * 50);
                posY = ((Math.abs(vector.y)) * 50) + 50;
            }

            if (q == "q4") {
                posX = ((Math.abs(vector.x)) * 50) + 50;
                posY = ((Math.abs(vector.y)) * 50) + 50;
            }

            tooltip.style.left = posX + '%';
            tooltip.style.top = posY + '%';

            document.body.appendChild(tooltip);
        }
        //remove tooltip if mouse does not hover over the pie or a pie segment
        else if (status === "hide" && document.getElementById("tooltip")) {
            document.body.removeChild(document.getElementById("tooltip"));
        }
    }

    entryAnimation() {
        let cam = this.camera;
        let startPos = {x: cam.position.x, y: cam.position.y, z: cam.position.z};
        let endPos = {x: 0, y: -10, z: 7};

        new TWEEN.Tween(startPos)
            .to({x: endPos.x, y: endPos.y, z: endPos.z}, 2500)
            .easing(TWEEN.Easing.Cubic.Out)
            .onUpdate(function () {
                cam.position.setY(startPos.y);
                cam.position.setZ(startPos.z);
            })
            .delay(800)
            .start();

    }

    resetCameraPosition(){
        let cam = this.camera;
        let actualPos = {x: cam.position.x, y: cam.position.y, z: Math.ceil(cam.position.z)}; //ceiling upwards cause of minimal variety
        let defaultPos = {x: 0, y: -10, z: 7};
        let initPos = (actualPos.x == defaultPos.x && actualPos.y == defaultPos.y && actualPos.z == defaultPos.z);

        if(!initPos) {
            new TWEEN.Tween(actualPos)
                .to({x: defaultPos.x, y: defaultPos.y, z: defaultPos.z}, 4000)
                .easing(TWEEN.Easing.Cubic.Out)
                .onUpdate(function () {
                    cam.position.set(actualPos.x,actualPos.y,actualPos.z);
                }).start();
        }
    }

    /*resetObjectPosition(){
        let object = this.scene.getObjectByName("groupedPieChart", true);
        let actualPos = {x: object.rotation.x, y: object.rotation.y, z: object.rotation.z};
        let defaultPos = {x: 0, y: 0, z: 0};

        let initPos = (actualPos.x == defaultPos.x && actualPos.y == defaultPos.y && actualPos.z == defaultPos.z);

        if(!initPos) {
            new TWEEN.Tween(actualPos)
                .to({x: defaultPos.x, y: defaultPos.y, z: defaultPos.z}, 4000)
                .easing(TWEEN.Easing.Cubic.Out)
                .onUpdate(function () {
                    object.rotation.set(actualPos.x,actualPos.y,actualPos.z);
                }).start();
        }
    }*/

}

export {SceneInit}
<|endoftext|>


<commit_before>define(function(require) {

  var $ = require('jquery');
  var _ = require('underscore');
  var Backbone = require('backbone');
  var EntriesView = require('views/entries');
  var EditorView = require('views/editor');
  var docs = require('collections/docs');
  var cache = require('utils/cache');
  var settings = require('models/settings');

  var AppView = Backbone.View.extend({

    el: 'body',

    initialize: function() {
      this.editor = new EditorView();
      this.entries = new EntriesView();

      this.$menuButton = $('#menu-button');

      if (!cache.isMobile) {

        setTimeout(_.bind(function() {
          this.aside('hidden');
        }, this), 3000);

        //hide sidebar when 3 or more docs and the open doc is not empty
        docs.on('change:title', function() {
            setTimeout(_.bind(function() {
              this.aside('hidden');
            }, this), 1000);
        }, this);

      }

      var showAside = function() {
        if (!cache.isMobile) this.aside('visible');
      };
      docs.on('add', showAside, this);
      docs.on('fetch', showAside, this);

      this.editor.$el.scroll(_.bind(this.toggleMenuButton, this));
      this.entries.$el.scroll(_.bind(this.toggleMenuButton, this));

    },

    aside: (function() {
      var $aside = this.$('aside');
      return function(state) {
        if (!state) {
          $aside.toggleClass('visible');
        } else if (state === 'visible') {
          $aside.addClass('visible');
        } else if (state === 'hidden') {
          if (docs.length > 2 && !cache.openDoc.isEmpty()) {
            $aside.removeClass('visible');
          }
        }
      };
    })(),

    events: {
      'click #add': 'newDoc',
      'click #aside': 'toggleAside',
      'click #menu-button': 'toggleAside',
      'keydown': 'handleKey',
      'keyup': 'hideAside'
    },

    newDoc: function(e) {
      e.preventDefault();

      if (!cache.openDoc.isEmpty()) {
        docs.addNew();
      } else {
        this.editor.focus();
      }
    },

    toggleAside: function(e) {
      e.stopImmediatePropagation();
      this.$menuButton.fadeIn(); // TODO probably better to call the function directly
      this.aside();
    },

    toggleMenuButton: function(e) {
      var top = $(e.currentTarget).scrollTop();
      this.$menuButton[top > 20 ? 'fadeOut' : 'fadeIn' ]();
    },

    handleKey: function(e) {
      if (e.which === 9) { //tab
        e.preventDefault();
      } else if (e[cache.modKey.name]) {
        this.aside('visible');
        if (e.which === 78) { //n
          this.newDoc(e);
        } else if (e.which === 38) { //up
          this.openPreviousDoc();
          return false;
        } else if (e.which === 40) { //down
          this.openNextDoc();
          return false;
        }
      }
    },

    hideAside: function(e) {
      if (e.which === (cache.modKey.code)) this.aside('hidden');
    },

    openPreviousDoc: function() {
      var previousDoc = docs.at(docs.indexOf(cache.openDoc) - 1);
      if (previousDoc) {
        settings.save('openDocId', previousDoc.id);
      }
    },

    openNextDoc: function() {
      var nextDoc = docs.at(docs.indexOf(cache.openDoc) + 1);
      if (nextDoc) {
        settings.save('openDocId', nextDoc.id);
      }
    }
  });

  return AppView;
});
<commit_msg>only execude toggleAside on mobile<commit_after>define(function(require) {

  var $ = require('jquery');
  var _ = require('underscore');
  var Backbone = require('backbone');
  var EntriesView = require('views/entries');
  var EditorView = require('views/editor');
  var docs = require('collections/docs');
  var cache = require('utils/cache');
  var settings = require('models/settings');

  var AppView = Backbone.View.extend({

    el: 'body',

    initialize: function() {
      this.editor = new EditorView();
      this.entries = new EntriesView();

      this.$menuButton = $('#menu-button');

      if (!cache.isMobile) {

        setTimeout(_.bind(function() {
          this.aside('hidden');
        }, this), 3000);

        //hide sidebar when 3 or more docs and the open doc is not empty
        docs.on('change:title', function() {
            setTimeout(_.bind(function() {
              this.aside('hidden');
            }, this), 1000);
        }, this);

      }

      var showAside = function() {
        if (!cache.isMobile) this.aside('visible');
      };
      docs.on('add', showAside, this);
      docs.on('fetch', showAside, this);

      this.editor.$el.scroll(_.bind(this.toggleMenuButton, this));
      this.entries.$el.scroll(_.bind(this.toggleMenuButton, this));

    },

    aside: (function() {
      var $aside = this.$('aside');
      return function(state) {
        if (!state) {
          $aside.toggleClass('visible');
        } else if (state === 'visible') {
          $aside.addClass('visible');
        } else if (state === 'hidden') {
          if (docs.length > 2 && !cache.openDoc.isEmpty()) {
            $aside.removeClass('visible');
          }
        }
      };
    })(),

    events: {
      'click #add': 'newDoc',
      'click #aside': 'toggleAside',
      'click #menu-button': 'toggleAside',
      'keydown': 'handleKey',
      'keyup': 'hideAside'
    },

    newDoc: function(e) {
      e.preventDefault();

      if (!cache.openDoc.isEmpty()) {
        docs.addNew();
      } else {
        this.editor.focus();
      }
    },

    toggleAside: function(e) {
      if (cache.isMobile) {
        e.stopImmediatePropagation();
        this.$menuButton.fadeIn();
        this.aside();
      }
    },

    toggleMenuButton: function(e) {
      var top = $(e.currentTarget).scrollTop();
      this.$menuButton[top > 20 ? 'fadeOut' : 'fadeIn' ]();
    },

    handleKey: function(e) {
      if (e.which === 9) { //tab
        e.preventDefault();
      } else if (e[cache.modKey.name]) {
        this.aside('visible');
        if (e.which === 78) { //n
          this.newDoc(e);
        } else if (e.which === 38) { //up
          this.openPreviousDoc();
          return false;
        } else if (e.which === 40) { //down
          this.openNextDoc();
          return false;
        }
      }
    },

    hideAside: function(e) {
      if (e.which === (cache.modKey.code)) this.aside('hidden');
    },

    openPreviousDoc: function() {
      var previousDoc = docs.at(docs.indexOf(cache.openDoc) - 1);
      if (previousDoc) {
        settings.save('openDocId', previousDoc.id);
      }
    },

    openNextDoc: function() {
      var nextDoc = docs.at(docs.indexOf(cache.openDoc) + 1);
      if (nextDoc) {
        settings.save('openDocId', nextDoc.id);
      }
    }
  });

  return AppView;
});
<|endoftext|>


<commit_before>define(function(require) {

  var $ = require('jquery');
  var _ = require('underscore');
  var Backbone = require('backbone');
  var EntriesView = require('views/entries');
  var EditorView = require('views/editor');
  var docs = require('collections/docs');
  var cache = require('utils/cache');
  var settings = require('models/settings');

  var noDocs = function() {
    return docs.length < 2 && cache.openDoc.isEmpty();
  };

  var AppView = Backbone.View.extend({

    el: 'body',

    initialize: function() {
      this.editor = new EditorView();
      this.entries = new EntriesView();

      this.$menuButton = $('#menu-button');

      if (cache.isMobile) {

        cache.loading.done(_.bind(function() {
          //only one doc and it is empty
          if ( noDocs() ) {
            this.$menuButton.fadeOut();
          } else {
            this.aside('visible');
          }
        }, this));

      } else {

        this.aside('visible');

        setTimeout(_.bind(function() {
          this.aside('hidden');
        }, this), 3000);

      }

      docs.on('fetch', this.editor.render, this.editor);

      docs.on('change:title', function() {
          setTimeout(_.bind(function() {
            if (cache.isMobile) {
              this.$menuButton[noDocs() ? 'fadeOut' : 'fadeIn']();
            } else {
              this.aside('hidden');
            }
          }, this), 1000);
      }, this);

      var showAside = function() {
        if (!cache.isMobile) this.aside('visible');
      };
      docs.on('add', showAside, this);
      docs.on('fetch', showAside, this);

      this.editor.$el.scroll(_.bind(this.toggleHeader, this));

    },

    aside: (function() {
      var $aside = $('body');
      return function(state) {
        if (!state) {
          $aside.toggleClass('visible');
        } else if (state === 'visible') {
          $aside.addClass('visible');
        } else if (state === 'hidden') {
          //hide sidebar when 3 or more docs and the open doc is not empty
          if (docs.length > 2 && !cache.openDoc.isEmpty()) {
            $aside.removeClass('visible');
          }
        }
      };
    })(),

    events: {
      'click #add': 'newDoc',
      'click #aside': 'toggleAside',
      'click #menu-button': 'toggleAside',
      'keydown': 'handleKey',
      'keyup': 'hideAside'
    },

    newDoc: function(e) {
      e.preventDefault();

      if (!cache.openDoc.isEmpty()) {
        docs.addNew();
      } else {
        this.editor.focus();
      }

    },

    toggleAside: function(e) {
      if (cache.isMobile) {
        e.stopImmediatePropagation();
        this.$menuButton.fadeIn();
        this.aside();
      }
    },

    toggleHeader: function(e) {
      if (cache.isMobile) {
        var top = $(e.currentTarget).scrollTop();
        this.$menuButton[top > 20 ? 'fadeOut' : 'fadeIn' ]();
        $('#remotestorage-connect')[top > 20 ? 'fadeOut' : 'fadeIn' ]();
      }
    },

    handleKey: function(e) {
      if (e.which === 9) { //tab
        e.preventDefault();
      } else if (e[cache.modKey.name]) {
        this.aside('visible');
        if (e.which === 78) { //n
          this.newDoc(e);
        } else if (e.which === 38) { //up
          this.openPreviousDoc();
          return false;
        } else if (e.which === 40) { //down
          this.openNextDoc();
          return false;
        }
      }
    },

    hideAside: function(e) {
      if (e.which === (cache.modKey.code)) this.aside('hidden');
    },

    openPreviousDoc: function() {
      var previousDoc = docs.at(docs.indexOf(cache.openDoc) - 1);
      if (previousDoc) {
        settings.save('openDocId', previousDoc.id);
      }
    },

    openNextDoc: function() {
      var nextDoc = docs.at(docs.indexOf(cache.openDoc) + 1);
      if (nextDoc) {
        settings.save('openDocId', nextDoc.id);
      }
    }
  });

  return AppView;
});
<commit_msg>remove toggleHeader JS to fully fix #46<commit_after>define(function(require) {

  var $ = require('jquery');
  var _ = require('underscore');
  var Backbone = require('backbone');
  var EntriesView = require('views/entries');
  var EditorView = require('views/editor');
  var docs = require('collections/docs');
  var cache = require('utils/cache');
  var settings = require('models/settings');

  var noDocs = function() {
    return docs.length < 2 && cache.openDoc.isEmpty();
  };

  var AppView = Backbone.View.extend({

    el: 'body',

    initialize: function() {
      this.editor = new EditorView();
      this.entries = new EntriesView();

      this.$menuButton = $('#menu-button');

      if (cache.isMobile) {

        cache.loading.done(_.bind(function() {
          //only one doc and it is empty
          if ( noDocs() ) {
            this.$menuButton.fadeOut();
          } else {
            this.aside('visible');
          }
        }, this));

      } else {

        this.aside('visible');

        setTimeout(_.bind(function() {
          this.aside('hidden');
        }, this), 3000);

      }

      docs.on('fetch', this.editor.render, this.editor);

      docs.on('change:title', function() {
          setTimeout(_.bind(function() {
            if (cache.isMobile) {
              this.$menuButton[noDocs() ? 'fadeOut' : 'fadeIn']();
            } else {
              this.aside('hidden');
            }
          }, this), 1000);
      }, this);

      var showAside = function() {
        if (!cache.isMobile) this.aside('visible');
      };
      docs.on('add', showAside, this);
      docs.on('fetch', showAside, this);

    },

    aside: (function() {
      var $aside = $('body');
      return function(state) {
        if (!state) {
          $aside.toggleClass('visible');
        } else if (state === 'visible') {
          $aside.addClass('visible');
        } else if (state === 'hidden') {
          //hide sidebar when 3 or more docs and the open doc is not empty
          if (docs.length > 2 && !cache.openDoc.isEmpty()) {
            $aside.removeClass('visible');
          }
        }
      };
    })(),

    events: {
      'click #add': 'newDoc',
      'click #aside': 'toggleAside',
      'click #menu-button': 'toggleAside',
      'keydown': 'handleKey',
      'keyup': 'hideAside'
    },

    newDoc: function(e) {
      e.preventDefault();

      if (!cache.openDoc.isEmpty()) {
        docs.addNew();
      } else {
        this.editor.focus();
      }

    },

    toggleAside: function(e) {
      if (cache.isMobile) {
        e.stopImmediatePropagation();
        this.$menuButton.fadeIn();
        this.aside();
      }
    },

    handleKey: function(e) {
      if (e.which === 9) { //tab
        e.preventDefault();
      } else if (e[cache.modKey.name]) {
        this.aside('visible');
        if (e.which === 78) { //n
          this.newDoc(e);
        } else if (e.which === 38) { //up
          this.openPreviousDoc();
          return false;
        } else if (e.which === 40) { //down
          this.openNextDoc();
          return false;
        }
      }
    },

    hideAside: function(e) {
      if (e.which === (cache.modKey.code)) this.aside('hidden');
    },

    openPreviousDoc: function() {
      var previousDoc = docs.at(docs.indexOf(cache.openDoc) - 1);
      if (previousDoc) {
        settings.save('openDocId', previousDoc.id);
      }
    },

    openNextDoc: function() {
      var nextDoc = docs.at(docs.indexOf(cache.openDoc) + 1);
      if (nextDoc) {
        settings.save('openDocId', nextDoc.id);
      }
    }
  });

  return AppView;
});
<|endoftext|>


<commit_before>import {
  addClass,
  removeClass,
  getClassNames,
  getElementPosition,
  detachNode,
  attachNode,
  cloneNode,
  waitUntilTransitionEnd,
  enableTransitions,
  disableTransitions,
  getTransitionDelay,
  forceReflow,
  insertAfter
} from './dom-manipulation';

/**
 * An animation which adds the `enter` classes and adds the element to the dom
 *
 * @param  {String} namespace       The animation class namespace e.g. 'am'
 * @param  {String[]} addClasses    Classes which will be added once the animation starts
 * @param  {String[]} removeClasses Classes which will be removed once the animation starts
 * @param  {HTMLElement} element    The element
 * @param  {HTMLElement} target     Optional - needed as reference for the new DOM position
 * @param  {"appendTo"|"prependTo"|"insertAfter"} operation  Dom operation
 * @return {Promise} A promise which will be resolved once the animation is complete
 */
export function enterAnimation ({
  namespace,
  addClasses,
  removeClasses,
  element,
  elementsCount,
  target,
  operation,
  animationIndex
}) {
  if (target) {
    attachNode(element, target, operation);
  }
  return animation({
    namespace,
    element,
    elementsCount,
    addClasses,
    removeClasses,
    animationIndex,
    animationName: 'enter'
  });
}

/**
 * An animation which adds the `leave` classes and adds the element to the dom
 *
 * @param  {String} namespace     The animation class namespace e.g. 'am'
 * @param  {HTMLElement} element  [description]
 * @param  {String[]} addClasses  Classes which will be added once the animation starts
 * @param  {String[]} removeClasses Classes which will be removed once the animation starts
 * @return {Promise} A promise which will be resolved once the animation is complete
 */
export function leaveAnimation ({
  namespace,
  element,
  elementsCount,
  addClasses,
  removeClasses,
  animationIndex
}) {
  return animation({
    namespace,
    element,
    elementsCount,
    addClasses,
    removeClasses,
    animationIndex,
    animationName: 'leave'
  });
}

/**
 * An animation which morphs an element from its old position to its new position
 *
 * @param  {String} namespace        The animation class namespace e.g. 'am'
 * @param  {String[]} addClasses     Classes which will be added once the animation starts
 * @param  {String[]} removeClasses  Classes which will be removed once the animation starts
 * @param  {HTMLElement} element     The element to animate
 * @param  {HTMLElement} target      The reference target to attach the element into the DOM
 * @param  {HTMLElement} operation   The attach method
 * @param  {HTMLElement} morphParent The wrapper for the move clone
 * @return {Promise} A promise which will be resolved once the animation is complete
 */
export function morphAnimation ({
  namespace,
  addClasses,
  removeClasses,
  element,
  elementsCount,
  target,
  operation,
  morphParent,
  animationIndex
}) {
  if (element instanceof window.HTMLElement === false) {
    throw new Error('target is required');
  }
  // Create clones which are needed for the morph effect
  const leavePlaceholder = _createLeavePlaceholder(element);
  const movePlaceholder = _createMovePlaceholder(element, morphParent);
  // Add final classes for the enter element to determine the correct target position
  addClasses.forEach((className) => addClass(element, className));
  removeClasses.forEach((className) => removeClass(element, className));
  // Wait for all animations to finish
  return Promise.all([
    enterAnimation({
      namespace,
      addClasses: [],
      removeClasses: [],
      element,
      elementsCount,
      target,
      operation,
      animationIndex
    }),
    moveAnimation({
      namespace,
      addClasses,
      removeClasses,
      element: movePlaceholder,
      elementsCount,
      morphParent,
      target: element,
      animationIndex
    }),
    removeAnimation({
      namespace,
      addClasses: [],
      removeClasses: [],
      element: leavePlaceholder,
      elementsCount,
      animationIndex
    })
  ]).then(() => {
    detachNode(movePlaceholder);
  });
}

/**
 * This animation will add the leave classes and remove the element form the
 * DOM once the animation is complete
 *
 * @param  {Object} options All leaveAnimation options
 * @return {Promise}
 */
export function removeAnimation (options) {
  return leaveAnimation(options).then(() => detachNode(options.element));
}

/**
 * Moves the given element to the target Position
 *
 * @param  {String} namespace     The animation class namespace e.g. 'am'
 * @param  {String[]} addClasses  Classes which will be added once the animation starts
 * @param  {String[]} removeClasses Classes which will be removed once the animation starts
 * @param  {HTMLElement} element  [description]
 * @param  {[type]} target        [description]
 * @param  {[type]} morphParent   [description]
 * @return {Promise} A promise which will be resolved once the animation is complete
 */
function moveAnimation ({
  namespace,
  addClasses,
  removeClasses,
  element,
  elementsCount,
  target,
  morphParent,
  animationIndex
}) {
  const targetPosition = getElementPosition(target);
  const parentPosition = getElementPosition(morphParent);
  const targetStyles = window.getComputedStyle(target);

  const top = targetPosition.top - parentPosition.top - parseFloat(targetStyles.marginTop);
  const left = targetPosition.left - parentPosition.left - parseFloat(targetStyles.marginLeft);

  return animation({
    namespace,
    element,
    elementsCount,
    addClasses,
    removeClasses,
    animationIndex,
    animationName: 'move',
    onAnimationStart: () => {
      element.style.position = 'absolute';
      element.style.left = `${left}px`;
      element.style.top = `${top}px`;
    }
  });
}

/**
 * Helper function which does the entire animation from start to cleanup
 *
 * @param  {String} namespace       The animation class namespace e.g. 'am'
 * @param  {String[]} addClasses    Classes which will be added once the animation starts
 * @param  {String[]} removeClasses Classes which will be removed once the animation starts
 * @param  {HTMLElement} element    The Element which to animate
 * @param  {HTMLElement} morphParent   [description]
 * @param  {function} [onAnimationStart] [description]
 * @return {Promise} A promise which will be resolved once the animation is complete
 */
function animation ({
  namespace,
  addClasses,
  removeClasses,
  element,
  elementsCount,
  animationName,
  animationIndex,
  onAnimationStart = () => {}
}) {
  return _startAnimation({
    namespace,
    element,
    elementsCount,
    addClasses,
    removeClasses,
    animationName,
    animationIndex
  })
  .then(onAnimationStart)
  .then(() => waitUntilTransitionEnd(element))
  .then(() => _removeAnimationClasses({
    namespace,
    element,
    animationName
  }));
}

function _createLeavePlaceholder (node) {
  const clone = cloneNode(node);
  insertAfter(node, clone);
  return clone;
}

/**
 * Create a clone and place it absolute directly above the given node
 *
 * @param  {HTMLElement} node        The node to clone
 * @param  {HTMLElement} morphParent The parent for the clone
 * @return {HTMLElement} clone
 */
function _createMovePlaceholder (node, morphParent) {
  const clone = cloneNode(node);
  const elementPosition = getElementPosition(node);
  const parentPosition = getElementPosition(morphParent);
  const nodeStyles = window.getComputedStyle(node);
  const top = elementPosition.top - parentPosition.top - parseFloat(nodeStyles.marginTop);
  const left = elementPosition.left - parentPosition.left - parseFloat(nodeStyles.marginLeft);

  clone.style.position = 'absolute';
  clone.style.left = `${left}px`;
  clone.style.top = `${top}px`;
  morphParent.appendChild(clone);
  return clone;
}

function _startAnimation ({
  namespace,
  element,
  elementsCount,
  addClasses,
  removeClasses,
  animationIndex,
  animationName = 'enter'
}) {
  disableTransitions(element);
  addClass(element, `${namespace}-${animationName}-prepare`);
  addClass(element, `${namespace}-${animationName}`);
  addClass(element, `${namespace}-animate`);

  const cacheKey = _composeCacheKey(element);
  const staggeringDuration = animationIndex === 0 ? 0 : _getStaggeringFromCache({element, animationIndex, animationName, namespace}, cacheKey);

  return new Promise((resolve) => {
    setTimeout(resolve, staggeringDuration);
  }).then(() => forceReflow(element).then(() => {
    forceReflow(element);
    // clear cache for animation-group when last element of this group finished
    if (elementsCount === animationIndex + 1) {
      delete staggeringCache[cacheKey];
    }
    removeClass(element, `${namespace}-${animationName}-prepare`);
    addClass(element, `${namespace}-${animationName}-active`);
    addClasses.forEach((className) => addClass(element, className));
    removeClasses.forEach((className) => removeClass(element, className));
    enableTransitions(element);
  }));
}

/**
 * Returns a cache-key based on the class-names
 * @param {HTMLElement} node The HTML-node to receive the classes from
 * @return {string} The key composed of the class-names of the node
 */
function _composeCacheKey (node) {
  var classNames = getClassNames(node);
  classNames.sort();
  return classNames.join(' ');
}

function _removeAnimationClasses ({element, animationName, namespace}) {
  disableTransitions(element);
  return forceReflow(element)
  .then(() => {
    removeClass(element, `${namespace}-animate`);
    removeClass(element, `${namespace}-${animationName}`);
    removeClass(element, `${namespace}-${animationName}-active`);
    enableTransitions(element);
  });
}

/**
 * Adds the stagger classes measure the stagger duration and removes
 * the classes again.
 */
function _getStaggering ({element, animationName, namespace}) {
  const delayWithoutStagger = getTransitionDelay(element);
  addClass(element, `${namespace}-stagger`);
  addClass(element, `${namespace}-${animationName}-stagger`);
  const delayWithStagger = getTransitionDelay(element);
  removeClass(element, `${namespace}-stagger`);
  removeClass(element, `${namespace}-${animationName}-stagger`);
  // If there is no difference with or without the stagger class
  // asume that there is no staggering
  return delayWithoutStagger[0] === delayWithStagger[0] ? 0 : delayWithStagger[0];
}

const staggeringCache = {};
/**
 * Returns the staggering duration from cache
 * The value is calculated if no duration is cache
 */
function _getStaggeringFromCache ({element, animationIndex, animationName, namespace}, cacheKey) {
  if (!staggeringCache[cacheKey]) {
    staggeringCache[cacheKey] = _getStaggering({element, animationIndex, animationName, namespace});
  }
  return staggeringCache[cacheKey] * animationIndex;
}
<commit_msg>remove cache-clear<commit_after>import {
  addClass,
  removeClass,
  getClassNames,
  getElementPosition,
  detachNode,
  attachNode,
  cloneNode,
  waitUntilTransitionEnd,
  enableTransitions,
  disableTransitions,
  getTransitionDelay,
  forceReflow,
  insertAfter
} from './dom-manipulation';

/**
 * An animation which adds the `enter` classes and adds the element to the dom
 *
 * @param  {String} namespace       The animation class namespace e.g. 'am'
 * @param  {String[]} addClasses    Classes which will be added once the animation starts
 * @param  {String[]} removeClasses Classes which will be removed once the animation starts
 * @param  {HTMLElement} element    The element
 * @param  {HTMLElement} target     Optional - needed as reference for the new DOM position
 * @param  {"appendTo"|"prependTo"|"insertAfter"} operation  Dom operation
 * @return {Promise} A promise which will be resolved once the animation is complete
 */
export function enterAnimation ({
  namespace,
  addClasses,
  removeClasses,
  element,
  elementsCount,
  target,
  operation,
  animationIndex
}) {
  if (target) {
    attachNode(element, target, operation);
  }
  return animation({
    namespace,
    element,
    elementsCount,
    addClasses,
    removeClasses,
    animationIndex,
    animationName: 'enter'
  });
}

/**
 * An animation which adds the `leave` classes and adds the element to the dom
 *
 * @param  {String} namespace     The animation class namespace e.g. 'am'
 * @param  {HTMLElement} element  [description]
 * @param  {String[]} addClasses  Classes which will be added once the animation starts
 * @param  {String[]} removeClasses Classes which will be removed once the animation starts
 * @return {Promise} A promise which will be resolved once the animation is complete
 */
export function leaveAnimation ({
  namespace,
  element,
  elementsCount,
  addClasses,
  removeClasses,
  animationIndex
}) {
  return animation({
    namespace,
    element,
    elementsCount,
    addClasses,
    removeClasses,
    animationIndex,
    animationName: 'leave'
  });
}

/**
 * An animation which morphs an element from its old position to its new position
 *
 * @param  {String} namespace        The animation class namespace e.g. 'am'
 * @param  {String[]} addClasses     Classes which will be added once the animation starts
 * @param  {String[]} removeClasses  Classes which will be removed once the animation starts
 * @param  {HTMLElement} element     The element to animate
 * @param  {HTMLElement} target      The reference target to attach the element into the DOM
 * @param  {HTMLElement} operation   The attach method
 * @param  {HTMLElement} morphParent The wrapper for the move clone
 * @return {Promise} A promise which will be resolved once the animation is complete
 */
export function morphAnimation ({
  namespace,
  addClasses,
  removeClasses,
  element,
  elementsCount,
  target,
  operation,
  morphParent,
  animationIndex
}) {
  if (element instanceof window.HTMLElement === false) {
    throw new Error('target is required');
  }
  // Create clones which are needed for the morph effect
  const leavePlaceholder = _createLeavePlaceholder(element);
  const movePlaceholder = _createMovePlaceholder(element, morphParent);
  // Add final classes for the enter element to determine the correct target position
  addClasses.forEach((className) => addClass(element, className));
  removeClasses.forEach((className) => removeClass(element, className));
  // Wait for all animations to finish
  return Promise.all([
    enterAnimation({
      namespace,
      addClasses: [],
      removeClasses: [],
      element,
      elementsCount,
      target,
      operation,
      animationIndex
    }),
    moveAnimation({
      namespace,
      addClasses,
      removeClasses,
      element: movePlaceholder,
      elementsCount,
      morphParent,
      target: element,
      animationIndex
    }),
    removeAnimation({
      namespace,
      addClasses: [],
      removeClasses: [],
      element: leavePlaceholder,
      elementsCount,
      animationIndex
    })
  ]).then(() => {
    detachNode(movePlaceholder);
  });
}

/**
 * This animation will add the leave classes and remove the element form the
 * DOM once the animation is complete
 *
 * @param  {Object} options All leaveAnimation options
 * @return {Promise}
 */
export function removeAnimation (options) {
  return leaveAnimation(options).then(() => detachNode(options.element));
}

/**
 * Moves the given element to the target Position
 *
 * @param  {String} namespace     The animation class namespace e.g. 'am'
 * @param  {String[]} addClasses  Classes which will be added once the animation starts
 * @param  {String[]} removeClasses Classes which will be removed once the animation starts
 * @param  {HTMLElement} element  [description]
 * @param  {[type]} target        [description]
 * @param  {[type]} morphParent   [description]
 * @return {Promise} A promise which will be resolved once the animation is complete
 */
function moveAnimation ({
  namespace,
  addClasses,
  removeClasses,
  element,
  elementsCount,
  target,
  morphParent,
  animationIndex
}) {
  const targetPosition = getElementPosition(target);
  const parentPosition = getElementPosition(morphParent);
  const targetStyles = window.getComputedStyle(target);

  const top = targetPosition.top - parentPosition.top - parseFloat(targetStyles.marginTop);
  const left = targetPosition.left - parentPosition.left - parseFloat(targetStyles.marginLeft);

  return animation({
    namespace,
    element,
    elementsCount,
    addClasses,
    removeClasses,
    animationIndex,
    animationName: 'move',
    onAnimationStart: () => {
      element.style.position = 'absolute';
      element.style.left = `${left}px`;
      element.style.top = `${top}px`;
    }
  });
}

/**
 * Helper function which does the entire animation from start to cleanup
 *
 * @param  {String} namespace       The animation class namespace e.g. 'am'
 * @param  {String[]} addClasses    Classes which will be added once the animation starts
 * @param  {String[]} removeClasses Classes which will be removed once the animation starts
 * @param  {HTMLElement} element    The Element which to animate
 * @param  {HTMLElement} morphParent   [description]
 * @param  {function} [onAnimationStart] [description]
 * @return {Promise} A promise which will be resolved once the animation is complete
 */
function animation ({
  namespace,
  addClasses,
  removeClasses,
  element,
  elementsCount,
  animationName,
  animationIndex,
  onAnimationStart = () => {}
}) {
  return _startAnimation({
    namespace,
    element,
    elementsCount,
    addClasses,
    removeClasses,
    animationName,
    animationIndex
  })
  .then(onAnimationStart)
  .then(() => waitUntilTransitionEnd(element))
  .then(() => _removeAnimationClasses({
    namespace,
    element,
    animationName
  }));
}

function _createLeavePlaceholder (node) {
  const clone = cloneNode(node);
  insertAfter(node, clone);
  return clone;
}

/**
 * Create a clone and place it absolute directly above the given node
 *
 * @param  {HTMLElement} node        The node to clone
 * @param  {HTMLElement} morphParent The parent for the clone
 * @return {HTMLElement} clone
 */
function _createMovePlaceholder (node, morphParent) {
  const clone = cloneNode(node);
  const elementPosition = getElementPosition(node);
  const parentPosition = getElementPosition(morphParent);
  const nodeStyles = window.getComputedStyle(node);
  const top = elementPosition.top - parentPosition.top - parseFloat(nodeStyles.marginTop);
  const left = elementPosition.left - parentPosition.left - parseFloat(nodeStyles.marginLeft);

  clone.style.position = 'absolute';
  clone.style.left = `${left}px`;
  clone.style.top = `${top}px`;
  morphParent.appendChild(clone);
  return clone;
}

function _startAnimation ({
  namespace,
  element,
  elementsCount,
  addClasses,
  removeClasses,
  animationIndex,
  animationName = 'enter'
}) {
  const cacheKey = _composeCacheKey(element, namespace, animationName);
  const staggeringDuration = animationIndex === 0 ? 0 : _getStaggeringFromCache({element, animationIndex, animationName, namespace}, cacheKey);

  disableTransitions(element);
  addClass(element, `${namespace}-${animationName}-prepare`);
  addClass(element, `${namespace}-${animationName}`);
  addClass(element, `${namespace}-animate`);

  return new Promise((resolve) => {
    setTimeout(resolve, staggeringDuration);
  }).then(() => forceReflow(element).then(() => {
    forceReflow(element);
    removeClass(element, `${namespace}-${animationName}-prepare`);
    addClass(element, `${namespace}-${animationName}-active`);
    addClasses.forEach((className) => addClass(element, className));
    removeClasses.forEach((className) => removeClass(element, className));
    enableTransitions(element);
  }));
}

/**
 * Returns a cache-key based on the class-names
 * @param {HTMLElement} node The HTML-node to receive the classes from
 * @return {string} The key composed of the class-names of the node
 */
function _composeCacheKey (node, namespace, animationName) {
  var classNames = getClassNames(node);
  classNames.push(`${namespace}-${animationName}`);
  classNames.sort();
  return classNames.join(' ');
}

function _removeAnimationClasses ({element, animationName, namespace}) {
  disableTransitions(element);
  return forceReflow(element)
  .then(() => {
    removeClass(element, `${namespace}-animate`);
    removeClass(element, `${namespace}-${animationName}`);
    removeClass(element, `${namespace}-${animationName}-active`);
    enableTransitions(element);
  });
}

/**
 * Adds the stagger classes measure the stagger duration and removes
 * the classes again.
 */
function _getStaggering ({element, animationName, namespace}) {
  const delayWithoutStagger = getTransitionDelay(element);
  addClass(element, `${namespace}-stagger`);
  addClass(element, `${namespace}-${animationName}-stagger`);
  const delayWithStagger = getTransitionDelay(element);
  removeClass(element, `${namespace}-stagger`);
  removeClass(element, `${namespace}-${animationName}-stagger`);
  // If there is no difference with or without the stagger class
  // asume that there is no staggering
  return delayWithoutStagger[0] === delayWithStagger[0] ? 0 : delayWithStagger[0];
}

const staggeringCache = {};
/**
 * Returns the staggering duration from cache
 * The value is calculated if no duration is cache
 */
function _getStaggeringFromCache ({element, animationIndex, animationName, namespace}, cacheKey) {
  if (!staggeringCache[cacheKey]) {
    staggeringCache[cacheKey] = _getStaggering({element, animationIndex, animationName, namespace});
  }
  return staggeringCache[cacheKey] * animationIndex;
}
<|endoftext|>