• 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<link rel="import" href="iron-overlay-backdrop.html">
14
15<script>
16
17  /**
18   * @struct
19   * @constructor
20   * @private
21   */
22  Polymer.IronOverlayManagerClass = function() {
23    /**
24     * Used to keep track of the opened overlays.
25     * @private {Array<Element>}
26     */
27    this._overlays = [];
28
29    /**
30     * iframes have a default z-index of 100,
31     * so this default should be at least that.
32     * @private {number}
33     */
34    this._minimumZ = 101;
35
36    /**
37     * Memoized backdrop element.
38     * @private {Element|null}
39     */
40    this._backdropElement = null;
41
42    // Enable document-wide tap recognizer.
43    // NOTE: Use useCapture=true to avoid accidentally prevention of the closing
44    // of an overlay via event.stopPropagation(). The only way to prevent
45    // closing of an overlay should be through its APIs.
46    // NOTE: enable tap on <html> to workaround Polymer/polymer#4459
47    Polymer.Gestures.add(document.documentElement, 'tap', null);
48    document.addEventListener('tap', this._onCaptureClick.bind(this), true);
49    document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
50    document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
51  };
52
53  Polymer.IronOverlayManagerClass.prototype = {
54
55    constructor: Polymer.IronOverlayManagerClass,
56
57    /**
58     * The shared backdrop element.
59     * @type {!Element} backdropElement
60     */
61    get backdropElement() {
62      if (!this._backdropElement) {
63        this._backdropElement = document.createElement('iron-overlay-backdrop');
64      }
65      return this._backdropElement;
66    },
67
68    /**
69     * The deepest active element.
70     * @type {!Element} activeElement the active element
71     */
72    get deepActiveElement() {
73      // document.activeElement can be null
74      // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
75      // In case of null, default it to document.body.
76      var active = document.activeElement || document.body;
77      while (active.root && Polymer.dom(active.root).activeElement) {
78        active = Polymer.dom(active.root).activeElement;
79      }
80      return active;
81    },
82
83    /**
84     * Brings the overlay at the specified index to the front.
85     * @param {number} i
86     * @private
87     */
88    _bringOverlayAtIndexToFront: function(i) {
89      var overlay = this._overlays[i];
90      if (!overlay) {
91        return;
92      }
93      var lastI = this._overlays.length - 1;
94      var currentOverlay = this._overlays[lastI];
95      // Ensure always-on-top overlay stays on top.
96      if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
97        lastI--;
98      }
99      // If already the top element, return.
100      if (i >= lastI) {
101        return;
102      }
103      // Update z-index to be on top.
104      var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
105      if (this._getZ(overlay) <= minimumZ) {
106        this._applyOverlayZ(overlay, minimumZ);
107      }
108
109      // Shift other overlays behind the new on top.
110      while (i < lastI) {
111        this._overlays[i] = this._overlays[i + 1];
112        i++;
113      }
114      this._overlays[lastI] = overlay;
115    },
116
117    /**
118     * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
119     * Also updates the backdrop z-index.
120     * @param {!Element} overlay
121     */
122    addOrRemoveOverlay: function(overlay) {
123      if (overlay.opened) {
124        this.addOverlay(overlay);
125      } else {
126        this.removeOverlay(overlay);
127      }
128    },
129
130    /**
131     * Tracks overlays for z-index and focus management.
132     * Ensures the last added overlay with always-on-top remains on top.
133     * @param {!Element} overlay
134     */
135    addOverlay: function(overlay) {
136      var i = this._overlays.indexOf(overlay);
137      if (i >= 0) {
138        this._bringOverlayAtIndexToFront(i);
139        this.trackBackdrop();
140        return;
141      }
142      var insertionIndex = this._overlays.length;
143      var currentOverlay = this._overlays[insertionIndex - 1];
144      var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
145      var newZ = this._getZ(overlay);
146
147      // Ensure always-on-top overlay stays on top.
148      if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
149        // This bumps the z-index of +2.
150        this._applyOverlayZ(currentOverlay, minimumZ);
151        insertionIndex--;
152        // Update minimumZ to match previous overlay's z-index.
153        var previousOverlay = this._overlays[insertionIndex - 1];
154        minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
155      }
156
157      // Update z-index and insert overlay.
158      if (newZ <= minimumZ) {
159        this._applyOverlayZ(overlay, minimumZ);
160      }
161      this._overlays.splice(insertionIndex, 0, overlay);
162
163      this.trackBackdrop();
164    },
165
166    /**
167     * @param {!Element} overlay
168     */
169    removeOverlay: function(overlay) {
170      var i = this._overlays.indexOf(overlay);
171      if (i === -1) {
172        return;
173      }
174      this._overlays.splice(i, 1);
175
176      this.trackBackdrop();
177    },
178
179    /**
180     * Returns the current overlay.
181     * @return {Element|undefined}
182     */
183    currentOverlay: function() {
184      var i = this._overlays.length - 1;
185      return this._overlays[i];
186    },
187
188    /**
189     * Returns the current overlay z-index.
190     * @return {number}
191     */
192    currentOverlayZ: function() {
193      return this._getZ(this.currentOverlay());
194    },
195
196    /**
197     * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
198     * This does not effect the z-index of any existing overlays.
199     * @param {number} minimumZ
200     */
201    ensureMinimumZ: function(minimumZ) {
202      this._minimumZ = Math.max(this._minimumZ, minimumZ);
203    },
204
205    focusOverlay: function() {
206      var current = /** @type {?} */ (this.currentOverlay());
207      if (current) {
208        current._applyFocus();
209      }
210    },
211
212    /**
213     * Updates the backdrop z-index.
214     */
215    trackBackdrop: function() {
216      var overlay = this._overlayWithBackdrop();
217      // Avoid creating the backdrop if there is no overlay with backdrop.
218      if (!overlay && !this._backdropElement) {
219        return;
220      }
221      this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
222      this.backdropElement.opened = !!overlay;
223    },
224
225    /**
226     * @return {Array<Element>}
227     */
228    getBackdrops: function() {
229      var backdrops = [];
230      for (var i = 0; i < this._overlays.length; i++) {
231        if (this._overlays[i].withBackdrop) {
232          backdrops.push(this._overlays[i]);
233        }
234      }
235      return backdrops;
236    },
237
238    /**
239     * Returns the z-index for the backdrop.
240     * @return {number}
241     */
242    backdropZ: function() {
243      return this._getZ(this._overlayWithBackdrop()) - 1;
244    },
245
246    /**
247     * Returns the first opened overlay that has a backdrop.
248     * @return {Element|undefined}
249     * @private
250     */
251    _overlayWithBackdrop: function() {
252      for (var i = 0; i < this._overlays.length; i++) {
253        if (this._overlays[i].withBackdrop) {
254          return this._overlays[i];
255        }
256      }
257    },
258
259    /**
260     * Calculates the minimum z-index for the overlay.
261     * @param {Element=} overlay
262     * @private
263     */
264    _getZ: function(overlay) {
265      var z = this._minimumZ;
266      if (overlay) {
267        var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
268        // Check if is a number
269        // Number.isNaN not supported in IE 10+
270        if (z1 === z1) {
271          z = z1;
272        }
273      }
274      return z;
275    },
276
277    /**
278     * @param {!Element} element
279     * @param {number|string} z
280     * @private
281     */
282    _setZ: function(element, z) {
283      element.style.zIndex = z;
284    },
285
286    /**
287     * @param {!Element} overlay
288     * @param {number} aboveZ
289     * @private
290     */
291    _applyOverlayZ: function(overlay, aboveZ) {
292      this._setZ(overlay, aboveZ + 2);
293    },
294
295    /**
296     * Returns the deepest overlay in the path.
297     * @param {Array<Element>=} path
298     * @return {Element|undefined}
299     * @suppress {missingProperties}
300     * @private
301     */
302    _overlayInPath: function(path) {
303      path = path || [];
304      for (var i = 0; i < path.length; i++) {
305        if (path[i]._manager === this) {
306          return path[i];
307        }
308      }
309    },
310
311    /**
312     * Ensures the click event is delegated to the right overlay.
313     * @param {!Event} event
314     * @private
315     */
316    _onCaptureClick: function(event) {
317      var overlay = /** @type {?} */ (this.currentOverlay());
318      // Check if clicked outside of top overlay.
319      if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
320        overlay._onCaptureClick(event);
321      }
322    },
323
324    /**
325     * Ensures the focus event is delegated to the right overlay.
326     * @param {!Event} event
327     * @private
328     */
329    _onCaptureFocus: function(event) {
330      var overlay = /** @type {?} */ (this.currentOverlay());
331      if (overlay) {
332        overlay._onCaptureFocus(event);
333      }
334    },
335
336    /**
337     * Ensures TAB and ESC keyboard events are delegated to the right overlay.
338     * @param {!Event} event
339     * @private
340     */
341    _onCaptureKeyDown: function(event) {
342      var overlay = /** @type {?} */ (this.currentOverlay());
343      if (overlay) {
344        if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
345          overlay._onCaptureEsc(event);
346        } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
347          overlay._onCaptureTab(event);
348        }
349      }
350    },
351
352    /**
353     * Returns if the overlay1 should be behind overlay2.
354     * @param {!Element} overlay1
355     * @param {!Element} overlay2
356     * @return {boolean}
357     * @suppress {missingProperties}
358     * @private
359     */
360    _shouldBeBehindOverlay: function(overlay1, overlay2) {
361      return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
362    }
363  };
364
365  Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
366</script>
367