| OLD | NEW |
| 1 (function() { | 1 (function() { |
| 2 'use strict'; | 2 'use strict'; |
| 3 // Used to calculate the scroll direction during touch events. |
| 4 var LAST_TOUCH_POSITION = { |
| 5 pageX: 0, |
| 6 pageY: 0 |
| 7 }; |
| 8 // Used to avoid computing event.path and filter scrollable nodes (better pe
rf). |
| 9 var ROOT_TARGET = null; |
| 10 var SCROLLABLE_NODES = []; |
| 3 | 11 |
| 4 /** | 12 /** |
| 5 * The IronDropdownScrollManager is intended to provide a central source | 13 * The IronDropdownScrollManager is intended to provide a central source |
| 6 * of authority and control over which elements in a document are currently | 14 * of authority and control over which elements in a document are currently |
| 7 * allowed to scroll. | 15 * allowed to scroll. |
| 8 */ | 16 */ |
| 9 | 17 |
| 10 Polymer.IronDropdownScrollManager = { | 18 Polymer.IronDropdownScrollManager = { |
| 11 | 19 |
| 12 /** | 20 /** |
| 13 * The current element that defines the DOM boundaries of the | 21 * The current element that defines the DOM boundaries of the |
| 14 * scroll lock. This is always the most recently locking element. | 22 * scroll lock. This is always the most recently locking element. |
| 15 */ | 23 */ |
| 16 get currentLockingElement() { | 24 get currentLockingElement() { |
| 17 return this._lockingElements[this._lockingElements.length - 1]; | 25 return this._lockingElements[this._lockingElements.length - 1]; |
| 18 }, | 26 }, |
| 19 | 27 |
| 20 | |
| 21 /** | 28 /** |
| 22 * Returns true if the provided element is "scroll locked," which is to | 29 * Returns true if the provided element is "scroll locked", which is to |
| 23 * say that it cannot be scrolled via pointer or keyboard interactions. | 30 * say that it cannot be scrolled via pointer or keyboard interactions. |
| 24 * | 31 * |
| 25 * @param {HTMLElement} element An HTML element instance which may or may | 32 * @param {HTMLElement} element An HTML element instance which may or may |
| 26 * not be scroll locked. | 33 * not be scroll locked. |
| 27 */ | 34 */ |
| 28 elementIsScrollLocked: function(element) { | 35 elementIsScrollLocked: function(element) { |
| 29 var currentLockingElement = this.currentLockingElement; | 36 var currentLockingElement = this.currentLockingElement; |
| 30 | 37 |
| 31 if (currentLockingElement === undefined) | 38 if (currentLockingElement === undefined) |
| 32 return false; | 39 return false; |
| (...skipping 72 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 105 this._unlockScrollInteractions(); | 112 this._unlockScrollInteractions(); |
| 106 } | 113 } |
| 107 }, | 114 }, |
| 108 | 115 |
| 109 _lockingElements: [], | 116 _lockingElements: [], |
| 110 | 117 |
| 111 _lockedElementCache: null, | 118 _lockedElementCache: null, |
| 112 | 119 |
| 113 _unlockedElementCache: null, | 120 _unlockedElementCache: null, |
| 114 | 121 |
| 115 _originalBodyStyles: {}, | |
| 116 | |
| 117 _isScrollingKeypress: function(event) { | 122 _isScrollingKeypress: function(event) { |
| 118 return Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys( | 123 return Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys( |
| 119 event, 'pageup pagedown home end up left down right'); | 124 event, 'pageup pagedown home end up left down right'); |
| 120 }, | 125 }, |
| 121 | 126 |
| 122 _hasCachedLockedElement: function(element) { | 127 _hasCachedLockedElement: function(element) { |
| 123 return this._lockedElementCache.indexOf(element) > -1; | 128 return this._lockedElementCache.indexOf(element) > -1; |
| 124 }, | 129 }, |
| 125 | 130 |
| 126 _hasCachedUnlockedElement: function(element) { | 131 _hasCachedUnlockedElement: function(element) { |
| (...skipping 27 matching lines...) Expand all Loading... |
| 154 if (this._composedTreeContains(distributedNodes[nodeIndex], child))
{ | 159 if (this._composedTreeContains(distributedNodes[nodeIndex], child))
{ |
| 155 return true; | 160 return true; |
| 156 } | 161 } |
| 157 } | 162 } |
| 158 } | 163 } |
| 159 | 164 |
| 160 return false; | 165 return false; |
| 161 }, | 166 }, |
| 162 | 167 |
| 163 _scrollInteractionHandler: function(event) { | 168 _scrollInteractionHandler: function(event) { |
| 164 var scrolledElement = | 169 // Avoid canceling an event with cancelable=false, e.g. scrolling is in |
| 165 /** @type {HTMLElement} */(Polymer.dom(event).rootTarget); | 170 // progress and cannot be interrupted. |
| 166 if (Polymer | 171 if (event.cancelable && this._shouldPreventScrolling(event)) { |
| 167 .IronDropdownScrollManager | |
| 168 .elementIsScrollLocked(scrolledElement)) { | |
| 169 if (event.type === 'keydown' && | |
| 170 !Polymer.IronDropdownScrollManager._isScrollingKeypress(event)) { | |
| 171 return; | |
| 172 } | |
| 173 | |
| 174 event.preventDefault(); | 172 event.preventDefault(); |
| 175 } | 173 } |
| 174 // If event has targetTouches (touch event), update last touch position. |
| 175 if (event.targetTouches) { |
| 176 var touch = event.targetTouches[0]; |
| 177 LAST_TOUCH_POSITION.pageX = touch.pageX; |
| 178 LAST_TOUCH_POSITION.pageY = touch.pageY; |
| 179 } |
| 176 }, | 180 }, |
| 177 | 181 |
| 178 _lockScrollInteractions: function() { | 182 _lockScrollInteractions: function() { |
| 179 // Memoize body inline styles: | 183 this._boundScrollHandler = this._boundScrollHandler || |
| 180 this._originalBodyStyles.overflow = document.body.style.overflow; | 184 this._scrollInteractionHandler.bind(this); |
| 181 this._originalBodyStyles.overflowX = document.body.style.overflowX; | |
| 182 this._originalBodyStyles.overflowY = document.body.style.overflowY; | |
| 183 | |
| 184 // Disable overflow scrolling on body: | |
| 185 // TODO(cdata): It is technically not sufficient to hide overflow on | |
| 186 // body alone. A better solution might be to traverse all ancestors of | |
| 187 // the current scroll locking element and hide overflow on them. This | |
| 188 // becomes expensive, though, as it would have to be redone every time | |
| 189 // a new scroll locking element is added. | |
| 190 document.body.style.overflow = 'hidden'; | |
| 191 document.body.style.overflowX = 'hidden'; | |
| 192 document.body.style.overflowY = 'hidden'; | |
| 193 | |
| 194 // Modern `wheel` event for mouse wheel scrolling: | 185 // Modern `wheel` event for mouse wheel scrolling: |
| 195 document.addEventListener('wheel', this._scrollInteractionHandler, true)
; | 186 document.addEventListener('wheel', this._boundScrollHandler, true); |
| 196 // Older, non-standard `mousewheel` event for some FF: | 187 // Older, non-standard `mousewheel` event for some FF: |
| 197 document.addEventListener('mousewheel', this._scrollInteractionHandler,
true); | 188 document.addEventListener('mousewheel', this._boundScrollHandler, true); |
| 198 // IE: | 189 // IE: |
| 199 document.addEventListener('DOMMouseScroll', this._scrollInteractionHandl
er, true); | 190 document.addEventListener('DOMMouseScroll', this._boundScrollHandler, tr
ue); |
| 191 // Save the SCROLLABLE_NODES on touchstart, to be used on touchmove. |
| 192 document.addEventListener('touchstart', this._boundScrollHandler, true); |
| 200 // Mobile devices can scroll on touch move: | 193 // Mobile devices can scroll on touch move: |
| 201 document.addEventListener('touchmove', this._scrollInteractionHandler, t
rue); | 194 document.addEventListener('touchmove', this._boundScrollHandler, true); |
| 202 // Capture keydown to prevent scrolling keys (pageup, pagedown etc.) | 195 // Capture keydown to prevent scrolling keys (pageup, pagedown etc.) |
| 203 document.addEventListener('keydown', this._scrollInteractionHandler, tru
e); | 196 document.addEventListener('keydown', this._boundScrollHandler, true); |
| 204 }, | 197 }, |
| 205 | 198 |
| 206 _unlockScrollInteractions: function() { | 199 _unlockScrollInteractions: function() { |
| 207 document.body.style.overflow = this._originalBodyStyles.overflow; | 200 document.removeEventListener('wheel', this._boundScrollHandler, true); |
| 208 document.body.style.overflowX = this._originalBodyStyles.overflowX; | 201 document.removeEventListener('mousewheel', this._boundScrollHandler, tru
e); |
| 209 document.body.style.overflowY = this._originalBodyStyles.overflowY; | 202 document.removeEventListener('DOMMouseScroll', this._boundScrollHandler,
true); |
| 203 document.removeEventListener('touchstart', this._boundScrollHandler, tru
e); |
| 204 document.removeEventListener('touchmove', this._boundScrollHandler, true
); |
| 205 document.removeEventListener('keydown', this._boundScrollHandler, true); |
| 206 }, |
| 210 | 207 |
| 211 document.removeEventListener('wheel', this._scrollInteractionHandler, tr
ue); | 208 /** |
| 212 document.removeEventListener('mousewheel', this._scrollInteractionHandle
r, true); | 209 * Returns true if the event causes scroll outside the current locking |
| 213 document.removeEventListener('DOMMouseScroll', this._scrollInteractionHa
ndler, true); | 210 * element, e.g. pointer/keyboard interactions, or scroll "leaking" |
| 214 document.removeEventListener('touchmove', this._scrollInteractionHandler
, true); | 211 * outside the locking element when it is already at its scroll boundaries
. |
| 215 document.removeEventListener('keydown', this._scrollInteractionHandler,
true); | 212 * @param {!Event} event |
| 213 * @return {boolean} |
| 214 * @private |
| 215 */ |
| 216 _shouldPreventScrolling: function(event) { |
| 217 // Avoid expensive checks if the event is not one of the observed keys. |
| 218 if (event.type === 'keydown') { |
| 219 // Prevent event if it is one of the scrolling keys. |
| 220 return this._isScrollingKeypress(event); |
| 221 } |
| 222 |
| 223 // Update if root target changed. For touch events, ensure we don't |
| 224 // update during touchmove. |
| 225 var target = Polymer.dom(event).rootTarget; |
| 226 if (event.type !== 'touchmove' && ROOT_TARGET !== target) { |
| 227 ROOT_TARGET = target; |
| 228 SCROLLABLE_NODES = this._getScrollableNodes(Polymer.dom(event).path); |
| 229 } |
| 230 |
| 231 // Prevent event if no scrollable nodes. |
| 232 if (!SCROLLABLE_NODES.length) { |
| 233 return true; |
| 234 } |
| 235 // Don't prevent touchstart event inside the locking element when it has |
| 236 // scrollable nodes. |
| 237 if (event.type === 'touchstart') { |
| 238 return false; |
| 239 } |
| 240 // Get deltaX/Y. |
| 241 var info = this._getScrollInfo(event); |
| 242 // Prevent if there is no child that can scroll. |
| 243 return !this._getScrollingNode(SCROLLABLE_NODES, info.deltaX, info.delta
Y); |
| 244 }, |
| 245 |
| 246 /** |
| 247 * Returns an array of scrollable nodes up to the current locking element, |
| 248 * which is included too if scrollable. |
| 249 * @param {!Array<Node>} nodes |
| 250 * @return {Array<Node>} scrollables |
| 251 * @private |
| 252 */ |
| 253 _getScrollableNodes: function(nodes) { |
| 254 var scrollables = []; |
| 255 var lockingIndex = nodes.indexOf(this.currentLockingElement); |
| 256 // Loop from root target to locking element (included). |
| 257 for (var i = 0; i <= lockingIndex; i++) { |
| 258 var node = nodes[i]; |
| 259 // Skip document fragments. |
| 260 if (node.nodeType === 11) { |
| 261 continue; |
| 262 } |
| 263 // Check inline style before checking computed style. |
| 264 var style = node.style; |
| 265 if (style.overflow !== 'scroll' && style.overflow !== 'auto') { |
| 266 style = window.getComputedStyle(node); |
| 267 } |
| 268 if (style.overflow === 'scroll' || style.overflow === 'auto') { |
| 269 scrollables.push(node); |
| 270 } |
| 271 } |
| 272 return scrollables; |
| 273 }, |
| 274 |
| 275 /** |
| 276 * Returns the node that is scrolling. If there is no scrolling, |
| 277 * returns undefined. |
| 278 * @param {!Array<Node>} nodes |
| 279 * @param {number} deltaX Scroll delta on the x-axis |
| 280 * @param {number} deltaY Scroll delta on the y-axis |
| 281 * @return {Node|undefined} |
| 282 * @private |
| 283 */ |
| 284 _getScrollingNode: function(nodes, deltaX, deltaY) { |
| 285 // No scroll. |
| 286 if (!deltaX && !deltaY) { |
| 287 return; |
| 288 } |
| 289 // Check only one axis according to where there is more scroll. |
| 290 // Prefer vertical to horizontal. |
| 291 var verticalScroll = Math.abs(deltaY) >= Math.abs(deltaX); |
| 292 for (var i = 0; i < nodes.length; i++) { |
| 293 var node = nodes[i]; |
| 294 var canScroll = false; |
| 295 if (verticalScroll) { |
| 296 // delta < 0 is scroll up, delta > 0 is scroll down. |
| 297 canScroll = deltaY < 0 ? node.scrollTop > 0 : |
| 298 node.scrollTop < node.scrollHeight - node.clientHeight; |
| 299 } else { |
| 300 // delta < 0 is scroll left, delta > 0 is scroll right. |
| 301 canScroll = deltaX < 0 ? node.scrollLeft > 0 : |
| 302 node.scrollLeft < node.scrollWidth - node.clientWidth; |
| 303 } |
| 304 if (canScroll) { |
| 305 return node; |
| 306 } |
| 307 } |
| 308 }, |
| 309 |
| 310 /** |
| 311 * Returns scroll `deltaX` and `deltaY`. |
| 312 * @param {!Event} event The scroll event |
| 313 * @return {{ |
| 314 * deltaX: number The x-axis scroll delta (positive: scroll right, |
| 315 * negative: scroll left, 0: no scroll), |
| 316 * deltaY: number The y-axis scroll delta (positive: scroll down, |
| 317 * negative: scroll up, 0: no scroll) |
| 318 * }} info |
| 319 * @private |
| 320 */ |
| 321 _getScrollInfo: function(event) { |
| 322 var info = { |
| 323 deltaX: event.deltaX, |
| 324 deltaY: event.deltaY |
| 325 }; |
| 326 // Already available. |
| 327 if ('deltaX' in event) { |
| 328 // do nothing, values are already good. |
| 329 } |
| 330 // Safari has scroll info in `wheelDeltaX/Y`. |
| 331 else if ('wheelDeltaX' in event) { |
| 332 info.deltaX = -event.wheelDeltaX; |
| 333 info.deltaY = -event.wheelDeltaY; |
| 334 } |
| 335 // Firefox has scroll info in `detail` and `axis`. |
| 336 else if ('axis' in event) { |
| 337 info.deltaX = event.axis === 1 ? event.detail : 0; |
| 338 info.deltaY = event.axis === 2 ? event.detail : 0; |
| 339 } |
| 340 // On mobile devices, calculate scroll direction. |
| 341 else if (event.targetTouches) { |
| 342 var touch = event.targetTouches[0]; |
| 343 // Touch moves from right to left => scrolling goes right. |
| 344 info.deltaX = LAST_TOUCH_POSITION.pageX - touch.pageX; |
| 345 // Touch moves from down to up => scrolling goes down. |
| 346 info.deltaY = LAST_TOUCH_POSITION.pageY - touch.pageY; |
| 347 } |
| 348 return info; |
| 216 } | 349 } |
| 217 }; | 350 }; |
| 218 })(); | 351 })(); |
| OLD | NEW |