Update Polymer and pull in iron-list
[chromium-blink-merge.git] / third_party / polymer / v1_0 / components-chromium / iron-dropdown / iron-dropdown-scroll-manager-extracted.js
blob5dbb0ddc7088fa9092df43fbd66d307639479978
2   (function() {
3     'use strict';
5     /**
6      * The IronDropdownScrollManager is intended to provide a central source
7      * of authority and control over which elements in a document are currently
8      * allowed to scroll.
9      */
11     Polymer.IronDropdownScrollManager = {
13       /**
14        * The current element that defines the DOM boundaries of the
15        * scroll lock. This is always the most recently locking element.
16        */
17       get currentLockingElement() {
18         return this._lockingElements[this._lockingElements.length - 1];
19       },
22       /**
23        * Returns true if the provided element is "scroll locked," which is to
24        * say that it cannot be scrolled via pointer or keyboard interactions.
25        *
26        * @param {HTMLElement} element An HTML element instance which may or may
27        * not be scroll locked.
28        */
29       elementIsScrollLocked: function(element) {
30         var currentLockingElement = this.currentLockingElement;
31         var scrollLocked;
33         if (this._hasCachedLockedElement(element)) {
34           return true;
35         }
37         if (this._hasCachedUnlockedElement(element)) {
38           return false;
39         }
41         scrollLocked = !!currentLockingElement &&
42           currentLockingElement !== element &&
43           !this._composedTreeContains(currentLockingElement, element);
45         if (scrollLocked) {
46           this._lockedElementCache.push(element);
47         } else {
48           this._unlockedElementCache.push(element);
49         }
51         return scrollLocked;
52       },
54       /**
55        * Push an element onto the current scroll lock stack. The most recently
56        * pushed element and its children will be considered scrollable. All
57        * other elements will not be scrollable.
58        *
59        * Scroll locking is implemented as a stack so that cases such as
60        * dropdowns within dropdowns are handled well.
61        *
62        * @param {HTMLElement} element The element that should lock scroll.
63        */
64       pushScrollLock: function(element) {
65         if (this._lockingElements.length === 0) {
66           this._lockScrollInteractions();
67         }
69         this._lockingElements.push(element);
71         this._lockedElementCache = [];
72         this._unlockedElementCache = [];
73       },
75       /**
76        * Remove an element from the scroll lock stack. The element being
77        * removed does not need to be the most recently pushed element. However,
78        * the scroll lock constraints only change when the most recently pushed
79        * element is removed.
80        *
81        * @param {HTMLElement} element The element to remove from the scroll
82        * lock stack.
83        */
84       removeScrollLock: function(element) {
85         var index = this._lockingElements.indexOf(element);
87         if (index === -1) {
88           return;
89         }
91         this._lockingElements.splice(index, 1);
93         this._lockedElementCache = [];
94         this._unlockedElementCache = [];
96         if (this._lockingElements.length === 0) {
97           this._unlockScrollInteractions();
98         }
99       },
101       _lockingElements: [],
103       _lockedElementCache: null,
105       _unlockedElementCache: null,
107       _originalBodyStyles: {},
109       _isScrollingKeypress: function(event) {
110         return Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(
111           event, 'pageup pagedown home end up left down right');
112       },
114       _hasCachedLockedElement: function(element) {
115         return this._lockedElementCache.indexOf(element) > -1;
116       },
118       _hasCachedUnlockedElement: function(element) {
119         return this._unlockedElementCache.indexOf(element) > -1;
120       },
122       _composedTreeContains: function(element, child) {
123         // NOTE(cdata): This method iterates over content elements and their
124         // corresponding distributed nodes to implement a contains-like method
125         // that pierces through the composed tree of the ShadowDOM. Results of
126         // this operation are cached (elsewhere) on a per-scroll-lock basis, to
127         // guard against potentially expensive lookups happening repeatedly as
128         // a user scrolls / touchmoves.
129         var contentElements;
130         var distributedNodes;
131         var contentIndex;
132         var nodeIndex;
134         if (element.contains(child)) {
135           return true;
136         }
138         contentElements = Polymer.dom(element).querySelectorAll('content');
140         for (contentIndex = 0; contentIndex < contentElements.length; ++contentIndex) {
142           distributedNodes = Polymer.dom(contentElements[contentIndex]).getDistributedNodes();
144           for (nodeIndex = 0; nodeIndex < distributedNodes.length; ++nodeIndex) {
146             if (this._composedTreeContains(distributedNodes[nodeIndex], child)) {
147               return true;
148             }
149           }
150         }
152         return false;
153       },
155       _scrollInteractionHandler: function(event) {
156         if (Polymer
157               .IronDropdownScrollManager
158               .elementIsScrollLocked(event.target)) {
159           if (event.type === 'keydown' &&
160               !Polymer.IronDropdownScrollManager._isScrollingKeypress(event)) {
161             return;
162           }
164           event.preventDefault();
165         }
166       },
168       _lockScrollInteractions: function() {
169         // Memoize body inline styles:
170         this._originalBodyStyles.overflow = document.body.style.overflow;
171         this._originalBodyStyles.overflowX = document.body.style.overflowX;
172         this._originalBodyStyles.overflowY = document.body.style.overflowY;
174         // Disable overflow scrolling on body:
175         // TODO(cdata): It is technically not sufficient to hide overflow on
176         // body alone. A better solution might be to traverse all ancestors of
177         // the current scroll locking element and hide overflow on them. This
178         // becomes expensive, though, as it would have to be redone every time
179         // a new scroll locking element is added.
180         document.body.style.overflow = 'hidden';
181         document.body.style.overflowX = 'hidden';
182         document.body.style.overflowY = 'hidden';
184         // Modern `wheel` event for mouse wheel scrolling:
185         window.addEventListener('wheel', this._scrollInteractionHandler, true);
186         // Older, non-standard `mousewheel` event for some FF:
187         window.addEventListener('mousewheel', this._scrollInteractionHandler, true);
188         // IE:
189         window.addEventListener('DOMMouseScroll', this._scrollInteractionHandler, true);
190         // Mobile devices can scroll on touch move:
191         window.addEventListener('touchmove', this._scrollInteractionHandler, true);
192         // Capture keydown to prevent scrolling keys (pageup, pagedown etc.)
193         document.addEventListener('keydown', this._scrollInteractionHandler, true);
194       },
196       _unlockScrollInteractions: function() {
197         document.body.style.overflow = this._originalBodyStyles.overflow;
198         document.body.style.overflowX = this._originalBodyStyles.overflowX;
199         document.body.style.overflowY = this._originalBodyStyles.overflowY;
201         window.removeEventListener('wheel', this._scrollInteractionHandler, true);
202         window.removeEventListener('mousewheel', this._scrollInteractionHandler, true);
203         window.removeEventListener('DOMMouseScroll', this._scrollInteractionHandler, true);
204         window.removeEventListener('touchmove', this._scrollInteractionHandler, true);
205         document.removeEventListener('keydown', this._scrollInteractionHandler, true);
206       }
207     };
208   })();