<!– @license Copyright © 2015 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at polymer.github.io/LICENSE.txt The complete set of authors may be found at polymer.github.io/AUTHORS.txt The complete set of contributors may be found at polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at polymer.github.io/PATENTS.txt –>

<link rel=“import” href=“../polymer/polymer.html”>

<script>

(function() {
  'use strict';
  // Used to calculate the scroll direction during touch events.
  var LAST_TOUCH_POSITION = {
    pageX: 0,
    pageY: 0
  };
  // Used to avoid computing event.path and filter scrollable nodes (better perf).
  var ROOT_TARGET = null;
  var SCROLLABLE_NODES = [];

  /**
   * The IronDropdownScrollManager is intended to provide a central source
   * of authority and control over which elements in a document are currently
   * allowed to scroll.
   */

  Polymer.IronDropdownScrollManager = {

    /**
     * The current element that defines the DOM boundaries of the
     * scroll lock. This is always the most recently locking element.
     */
    get currentLockingElement() {
      return this._lockingElements[this._lockingElements.length - 1];
    },

    /**
     * Returns true if the provided element is "scroll locked", which is to
     * say that it cannot be scrolled via pointer or keyboard interactions.
     *
     * @param {HTMLElement} element An HTML element instance which may or may
     * not be scroll locked.
     */
    elementIsScrollLocked: function(element) {
      var currentLockingElement = this.currentLockingElement;

      if (currentLockingElement === undefined)
        return false;

      var scrollLocked;

      if (this._hasCachedLockedElement(element)) {
        return true;
      }

      if (this._hasCachedUnlockedElement(element)) {
        return false;
      }

      scrollLocked = !!currentLockingElement &&
        currentLockingElement !== element &&
        !this._composedTreeContains(currentLockingElement, element);

      if (scrollLocked) {
        this._lockedElementCache.push(element);
      } else {
        this._unlockedElementCache.push(element);
      }

      return scrollLocked;
    },

    /**
     * Push an element onto the current scroll lock stack. The most recently
     * pushed element and its children will be considered scrollable. All
     * other elements will not be scrollable.
     *
     * Scroll locking is implemented as a stack so that cases such as
     * dropdowns within dropdowns are handled well.
     *
     * @param {HTMLElement} element The element that should lock scroll.
     */
    pushScrollLock: function(element) {
      // Prevent pushing the same element twice
      if (this._lockingElements.indexOf(element) >= 0) {
        return;
      }

      if (this._lockingElements.length === 0) {
        this._lockScrollInteractions();
      }

      this._lockingElements.push(element);

      this._lockedElementCache = [];
      this._unlockedElementCache = [];
    },

    /**
     * Remove an element from the scroll lock stack. The element being
     * removed does not need to be the most recently pushed element. However,
     * the scroll lock constraints only change when the most recently pushed
     * element is removed.
     *
     * @param {HTMLElement} element The element to remove from the scroll
     * lock stack.
     */
    removeScrollLock: function(element) {
      var index = this._lockingElements.indexOf(element);

      if (index === -1) {
        return;
      }

      this._lockingElements.splice(index, 1);

      this._lockedElementCache = [];
      this._unlockedElementCache = [];

      if (this._lockingElements.length === 0) {
        this._unlockScrollInteractions();
      }
    },

    _lockingElements: [],

    _lockedElementCache: null,

    _unlockedElementCache: null,

    _hasCachedLockedElement: function(element) {
      return this._lockedElementCache.indexOf(element) > -1;
    },

    _hasCachedUnlockedElement: function(element) {
      return this._unlockedElementCache.indexOf(element) > -1;
    },

    _composedTreeContains: function(element, child) {
      // NOTE(cdata): This method iterates over content elements and their
      // corresponding distributed nodes to implement a contains-like method
      // that pierces through the composed tree of the ShadowDOM. Results of
      // this operation are cached (elsewhere) on a per-scroll-lock basis, to
      // guard against potentially expensive lookups happening repeatedly as
      // a user scrolls / touchmoves.
      var contentElements;
      var distributedNodes;
      var contentIndex;
      var nodeIndex;

      if (element.contains(child)) {
        return true;
      }

      contentElements = Polymer.dom(element).querySelectorAll('content');

      for (contentIndex = 0; contentIndex < contentElements.length; ++contentIndex) {

        distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistributedNodes();

        for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {

          if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
            return true;
          }
        }
      }

      return false;
    },

    _scrollInteractionHandler: function(event) {
      // Avoid canceling an event with cancelable=false, e.g. scrolling is in
      // progress and cannot be interrupted.
      if (event.cancelable && this._shouldPreventScrolling(event)) {
        event.preventDefault();
      }
      // If event has targetTouches (touch event), update last touch position.
      if (event.targetTouches) {
        var touch = event.targetTouches[0];
        LAST_TOUCH_POSITION.pageX = touch.pageX;
        LAST_TOUCH_POSITION.pageY = touch.pageY;
      }
    },

    _lockScrollInteractions: function() {
      this._boundScrollHandler = this._boundScrollHandler ||
        this._scrollInteractionHandler.bind(this);
      // Modern `wheel` event for mouse wheel scrolling:
      document.addEventListener('wheel', this._boundScrollHandler, true);
      // Older, non-standard `mousewheel` event for some FF:
      document.addEventListener('mousewheel', this._boundScrollHandler, true);
      // IE:
      document.addEventListener('DOMMouseScroll', this._boundScrollHandler, true);
      // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove.
      document.addEventListener('touchstart', this._boundScrollHandler, true);
      // Mobile devices can scroll on touch move:
      document.addEventListener('touchmove', this._boundScrollHandler, true);
    },

    _unlockScrollInteractions: function() {
      document.removeEventListener('wheel', this._boundScrollHandler, true);
      document.removeEventListener('mousewheel', this._boundScrollHandler, true);
      document.removeEventListener('DOMMouseScroll', this._boundScrollHandler, true);
      document.removeEventListener('touchstart', this._boundScrollHandler, true);
      document.removeEventListener('touchmove', this._boundScrollHandler, true);
    },

    /**
     * Returns true if the event causes scroll outside the current locking
     * element, e.g. pointer/keyboard interactions, or scroll "leaking"
     * outside the locking element when it is already at its scroll boundaries.
     * @param {!Event} event
     * @return {boolean}
     * @private
     */
    _shouldPreventScrolling: function(event) {

      // Update if root target changed. For touch events, ensure we don't
      // update during touchmove.
      var target = Polymer.dom(event).rootTarget;
      if (event.type !== 'touchmove' && ROOT_TARGET !== target) {
        ROOT_TARGET = target;
        SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path);
      }

      // Prevent event if no scrollable nodes.
      if (!SCROLLABLE_NODES.length) {
        return true;
      }
      // Don't prevent touchstart event inside the locking element when it has
      // scrollable nodes.
      if (event.type === 'touchstart') {
        return false;
      }
      // Get deltaX/Y.
      var info = this._getScrollInfo(event);
      // Prevent if there is no child that can scroll.
      return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.deltaY);
    },

    /**
     * Returns an array of scrollable nodes up to the current locking element,
     * which is included too if scrollable.
     * @param {!Array<Node>} nodes
     * @return {Array<Node>} scrollables
     * @private
     */
    _getScrollableNodes: function(nodes) {
      var scrollables = [];
      var lockingIndex = nodes.indexOf(this.currentLockingElement);
      // Loop from root target to locking element (included).
      for (var i = 0; i <= lockingIndex; i++) {
        var node = nodes[i];
        // Skip document fragments.
        if (node.nodeType === 11) {
          continue;
        }
        // Check inline style before checking computed style.
        var style = node.style;
        if (style.overflow !== 'scroll' && style.overflow !== 'auto') {
          style = window.getComputedStyle(node);
        }
        if (style.overflow === 'scroll' || style.overflow === 'auto') {
          scrollables.push(node);
        }
      }
      return scrollables;
    },

    /**
     * Returns the node that is scrolling. If there is no scrolling,
     * returns undefined.
     * @param {!Array<Node>} nodes
     * @param {number} deltaX Scroll delta on the x-axis
     * @param {number} deltaY Scroll delta on the y-axis
     * @return {Node|undefined}
     * @private
     */
    _getScrollingNode: function(nodes, deltaX, deltaY) {
      // No scroll.
      if (!deltaX && !deltaY) {
        return;
      }
      // Check only one axis according to where there is more scroll.
      // Prefer vertical to horizontal.
      var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX);
      for (var i = 0; i < nodes.length; i++) {
        var node = nodes[i];
        var canScroll = false;
        if (verticalScroll) {
          // delta < 0 is scroll up, delta > 0 is scroll down.
          canScroll = deltaY < 0 ? node.scrollTop > 0 :
            node.scrollTop < node.scrollHeight - node.clientHeight;
        } else {
          // delta < 0 is scroll left, delta > 0 is scroll right.
          canScroll = deltaX < 0 ? node.scrollLeft > 0 :
            node.scrollLeft < node.scrollWidth - node.clientWidth;
        }
        if (canScroll) {
          return node;
        }
      }
    },

    /**
     * Returns scroll `deltaX` and `deltaY`.
     * @param {!Event} event The scroll event
     * @return {{deltaX: number, deltaY: number}} Object containing the
     * x-axis scroll delta (positive: scroll right, negative: scroll left,
     * 0: no scroll), and the y-axis scroll delta (positive: scroll down,
     * negative: scroll up, 0: no scroll).
     * @private
     */
    _getScrollInfo: function(event) {
      var info = {
        deltaX: event.deltaX,
        deltaY: event.deltaY
      };
      // Already available.
      if ('deltaX' in event) {
        // do nothing, values are already good.
      }
      // Safari has scroll info in `wheelDeltaX/Y`.
      else if ('wheelDeltaX' in event) {
        info.deltaX = -event.wheelDeltaX;
        info.deltaY = -event.wheelDeltaY;
      }
      // Firefox has scroll info in `detail` and `axis`.
      else if ('axis' in event) {
        info.deltaX = event.axis === 1 ? event.detail : 0;
        info.deltaY = event.axis === 2 ? event.detail : 0;
      }
      // On mobile devices, calculate scroll direction.
      else if (event.targetTouches) {
        var touch = event.targetTouches[0];
        // Touch moves from right to left => scrolling goes right.
        info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX;
        // Touch moves from down to up => scrolling goes down.
        info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY;
      }
      return info;
    }
  };
})();

</script>