<!-- @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"> <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> <script> (function() { 'use strict'; /** * 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; 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) { 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, _originalBodyStyles: {}, _isScrollingKeypress: function(event) { return Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys( event, 'pageup pagedown home end up left down right'); }, _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) { if (Polymer .IronDropdownScrollManager .elementIsScrollLocked(event.target)) { if (event.type === 'keydown' && !Polymer.IronDropdownScrollManager._isScrollingKeypress(event)) { return; } event.preventDefault(); } }, _lockScrollInteractions: function() { // Memoize body inline styles: this._originalBodyStyles.overflow = document.body.style.overflow; this._originalBodyStyles.overflowX = document.body.style.overflowX; this._originalBodyStyles.overflowY = document.body.style.overflowY; // Disable overflow scrolling on body: // TODO(cdata): It is technically not sufficient to hide overflow on // body alone. A better solution might be to traverse all ancestors of // the current scroll locking element and hide overflow on them. This // becomes expensive, though, as it would have to be redone every time // a new scroll locking element is added. document.body.style.overflow = 'hidden'; document.body.style.overflowX = 'hidden'; document.body.style.overflowY = 'hidden'; // Modern `wheel` event for mouse wheel scrolling: window.addEventListener('wheel', this._scrollInteractionHandler, true); // Older, non-standard `mousewheel` event for some FF: window.addEventListener('mousewheel', this._scrollInteractionHandler, true); // IE: window.addEventListener('DOMMouseScroll', this._scrollInteractionHandler, true); // Mobile devices can scroll on touch move: window.addEventListener('touchmove', this._scrollInteractionHandler, true); // Capture keydown to prevent scrolling keys (pageup, pagedown etc.) document.addEventListener('keydown', this._scrollInteractionHandler, true); }, _unlockScrollInteractions: function() { document.body.style.overflow = this._originalBodyStyles.overflow; document.body.style.overflowX = this._originalBodyStyles.overflowX; document.body.style.overflowY = this._originalBodyStyles.overflowY; window.removeEventListener('wheel', this._scrollInteractionHandler, true); window.removeEventListener('mousewheel', this._scrollInteractionHandler, true); window.removeEventListener('DOMMouseScroll', this._scrollInteractionHandler, true); window.removeEventListener('touchmove', this._scrollInteractionHandler, true); document.removeEventListener('keydown', this._scrollInteractionHandler, true); } }; })(); </script>