373 lines
13 KiB
HTML
373 lines
13 KiB
HTML
<!--
|
|
@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>
|