diff options
Diffstat (limited to 'catapult/third_party/polymer/components/iron-dropdown/iron-dropdown-scroll-manager.html')
-rw-r--r-- | catapult/third_party/polymer/components/iron-dropdown/iron-dropdown-scroll-manager.html | 372 |
1 files changed, 372 insertions, 0 deletions
diff --git a/catapult/third_party/polymer/components/iron-dropdown/iron-dropdown-scroll-manager.html b/catapult/third_party/polymer/components/iron-dropdown/iron-dropdown-scroll-manager.html new file mode 100644 index 00000000..125bf2b3 --- /dev/null +++ b/catapult/third_party/polymer/components/iron-dropdown/iron-dropdown-scroll-manager.html @@ -0,0 +1,372 @@ +<!-- +@license +Copyright (c) 2015 The Polymer Project Authors. All rights reserved. +This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt +The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt +The complete set of contributors may be found at http://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 http://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. + * @type {!Object} + */ + var lastTouchPosition = { + pageX: 0, + pageY: 0 + }; + /** + * Used to avoid computing event.path and filter scrollable nodes (better perf). + * @type {?EventTarget} + */ + var lastRootTarget = null; + /** + * @type {!Array<Node>} + */ + var lastScrollableNodes = []; + + var scrollEvents = [ + // Modern `wheel` event for mouse wheel scrolling: + 'wheel', + // Older, non-standard `mousewheel` event for some FF: + 'mousewheel', + // IE: + 'DOMMouseScroll', + // Touch enabled devices + 'touchstart', + 'touchmove' + ]; + + /** + * 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]; + lastTouchPosition.pageX = touch.pageX; + lastTouchPosition.pageY = touch.pageY; + } + }, + + _lockScrollInteractions: function() { + this._boundScrollHandler = this._boundScrollHandler || + this._scrollInteractionHandler.bind(this); + for (var i = 0, l = scrollEvents.length; i < l; i++) { + // NOTE: browsers that don't support objects as third arg will + // interpret it as boolean, hence useCapture = true in this case. + document.addEventListener(scrollEvents[i], this._boundScrollHandler, { + capture: true, + passive: false + }); + } + }, + + _unlockScrollInteractions: function() { + for (var i = 0, l = scrollEvents.length; i < l; i++) { + // NOTE: browsers that don't support objects as third arg will + // interpret it as boolean, hence useCapture = true in this case. + document.removeEventListener(scrollEvents[i], this._boundScrollHandler, { + capture: true, + passive: false + }); + } + }, + + /** + * 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' && lastRootTarget !== target) { + lastRootTarget = target; + lastScrollableNodes = this._getScrollableNodes(Polymer.dom(event).path); + } + + // Prevent event if no scrollable nodes. + if (!lastScrollableNodes.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(lastScrollableNodes, 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++) { + // Skip non-Element nodes. + if (nodes[i].nodeType !== Node.ELEMENT_NODE) { + continue; + } + var node = /** @type {!Element} */ (nodes[i]); + // 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 = lastTouchPosition.pageX - touch.pageX; + // Touch moves from down to up => scrolling goes down. + info.deltaY = lastTouchPosition.pageY - touch.pageY; + } + return info; + } + }; + })(); +</script> |