• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1<!--
2@license
3Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7Code distributed by Google as part of the polymer project is also
8subject 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