1 <!-- 2 @license 3 Copyright (c) 2015 The Polymer Project Authors. All rights reserved. 4 This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt 5 The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt 6 The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt 7 Code distributed by Google as part of the polymer project is also 8 subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt 9 --> 10 11 <link rel="import" href="../polymer/polymer.html"> 12 <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> 13 14 <script> 15 (function() { 16 'use strict'; 17 18 /** 19 * The IronDropdownScrollManager is intended to provide a central source 20 * of authority and control over which elements in a document are currently 21 * allowed to scroll. 22 */ 23 24 Polymer.IronDropdownScrollManager = { 25 26 /** 27 * The current element that defines the DOM boundaries of the 28 * scroll lock. This is always the most recently locking element. 29 */ 30 get currentLockingElement() { 31 return this._lockingElements[this._lockingElements.length - 1]; 32 }, 33 34 35 /** 36 * Returns true if the provided element is "scroll locked," which is to 37 * say that it cannot be scrolled via pointer or keyboard interactions. 38 * 39 * @param {HTMLElement} element An HTML element instance which may or may 40 * not be scroll locked. 41 */ 42 elementIsScrollLocked: function(element) { 43 var currentLockingElement = this.currentLockingElement; 44 45 if (currentLockingElement === undefined) 46 return false; 47 48 var scrollLocked; 49 50 if (this._hasCachedLockedElement(element)) { 51 return true; 52 } 53 54 if (this._hasCachedUnlockedElement(element)) { 55 return false; 56 } 57 58 scrollLocked = !!currentLockingElement && 59 currentLockingElement !== element && 60 !this._composedTreeContains(currentLockingElement, element); 61 62 if (scrollLocked) { 63 this._lockedElementCache.push(element); 64 } else { 65 this._unlockedElementCache.push(element); 66 } 67 68 return scrollLocked; 69 }, 70 71 /** 72 * Push an element onto the current scroll lock stack. The most recently 73 * pushed element and its children will be considered scrollable. All 74 * other elements will not be scrollable. 75 * 76 * Scroll locking is implemented as a stack so that cases such as 77 * dropdowns within dropdowns are handled well. 78 * 79 * @param {HTMLElement} element The element that should lock scroll. 80 */ 81 pushScrollLock: function(element) { 82 // Prevent pushing the same element twice 83 if (this._lockingElements.indexOf(element) >= 0) { 84 return; 85 } 86 87 if (this._lockingElements.length === 0) { 88 this._lockScrollInteractions(); 89 } 90 91 this._lockingElements.push(element); 92 93 this._lockedElementCache = []; 94 this._unlockedElementCache = []; 95 }, 96 97 /** 98 * Remove an element from the scroll lock stack. The element being 99 * removed does not need to be the most recently pushed element. However, 100 * the scroll lock constraints only change when the most recently pushed 101 * element is removed. 102 * 103 * @param {HTMLElement} element The element to remove from the scroll 104 * lock stack. 105 */ 106 removeScrollLock: function(element) { 107 var index = this._lockingElements.indexOf(element); 108 109 if (index === -1) { 110 return; 111 } 112 113 this._lockingElements.splice(index, 1); 114 115 this._lockedElementCache = []; 116 this._unlockedElementCache = []; 117 118 if (this._lockingElements.length === 0) { 119 this._unlockScrollInteractions(); 120 } 121 }, 122 123 _lockingElements: [], 124 125 _lockedElementCache: null, 126 127 _unlockedElementCache: null, 128 129 _originalBodyStyles: {}, 130 131 _isScrollingKeypress: function(event) { 132 return Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys( 133 event, 'pageup pagedown home end up left down right'); 134 }, 135 136 _hasCachedLockedElement: function(element) { 137 return this._lockedElementCache.indexOf(element) > -1; 138 }, 139 140 _hasCachedUnlockedElement: function(element) { 141 return this._unlockedElementCache.indexOf(element) > -1; 142 }, 143 144 _composedTreeContains: function(element, child) { 145 // NOTE(cdata): This method iterates over content elements and their 146 // corresponding distributed nodes to implement a contains-like method 147 // that pierces through the composed tree of the ShadowDOM. Results of 148 // this operation are cached (elsewhere) on a per-scroll-lock basis, to 149 // guard against potentially expensive lookups happening repeatedly as 150 // a user scrolls / touchmoves. 151 var contentElements; 152 var distributedNodes; 153 var contentIndex; 154 var nodeIndex; 155 156 if (element.contains(child)) { 157 return true; 158 } 159 160 contentElements = Polymer.dom(element).querySelectorAll('content'); 161 162 for (contentIndex = 0; contentIndex < contentElements.length; ++contentIndex) { 163 164 distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistributedNodes(); 165 166 for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) { 167 168 if (this._composedTreeContains(distributedNodes[nodeIndex], child)) { 169 return true; 170 } 171 } 172 } 173 174 return false; 175 }, 176 177 _scrollInteractionHandler: function(event) { 178 var scrolledElement = 179 /** @type {HTMLElement} */(Polymer.dom(event).rootTarget); 180 if (Polymer 181 .IronDropdownScrollManager 182 .elementIsScrollLocked(scrolledElement)) { 183 if (event.type === 'keydown' && 184 !Polymer.IronDropdownScrollManager._isScrollingKeypress(event)) { 185 return; 186 } 187 188 event.preventDefault(); 189 } 190 }, 191 192 _lockScrollInteractions: function() { 193 // Memoize body inline styles: 194 this._originalBodyStyles.overflow = document.body.style.overflow; 195 this._originalBodyStyles.overflowX = document.body.style.overflowX; 196 this._originalBodyStyles.overflowY = document.body.style.overflowY; 197 198 // Disable overflow scrolling on body: 199 // TODO(cdata): It is technically not sufficient to hide overflow on 200 // body alone. A better solution might be to traverse all ancestors of 201 // the current scroll locking element and hide overflow on them. This 202 // becomes expensive, though, as it would have to be redone every time 203 // a new scroll locking element is added. 204 document.body.style.overflow = 'hidden'; 205 document.body.style.overflowX = 'hidden'; 206 document.body.style.overflowY = 'hidden'; 207 208 // Modern `wheel` event for mouse wheel scrolling: 209 document.addEventListener('wheel', this._scrollInteractionHandler, true); 210 // Older, non-standard `mousewheel` event for some FF: 211 document.addEventListener('mousewheel', this._scrollInteractionHandler, true); 212 // IE: 213 document.addEventListener('DOMMouseScroll', this._scrollInteractionHandler, true); 214 // Mobile devices can scroll on touch move: 215 document.addEventListener('touchmove', this._scrollInteractionHandler, true); 216 // Capture keydown to prevent scrolling keys (pageup, pagedown etc.) 217 document.addEventListener('keydown', this._scrollInteractionHandler, true); 218 }, 219 220 _unlockScrollInteractions: function() { 221 document.body.style.overflow = this._originalBodyStyles.overflow; 222 document.body.style.overflowX = this._originalBodyStyles.overflowX; 223 document.body.style.overflowY = this._originalBodyStyles.overflowY; 224 225 document.removeEventListener('wheel', this._scrollInteractionHandler, true); 226 document.removeEventListener('mousewheel', this._scrollInteractionHandler, true); 227 document.removeEventListener('DOMMouseScroll', this._scrollInteractionHandler, true); 228 document.removeEventListener('touchmove', this._scrollInteractionHandler, true); 229 document.removeEventListener('keydown', this._scrollInteractionHandler, true); 230 } 231 }; 232 })(); 233 </script> 234