• 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    Polymer.Gestures.add(document, 'tap', null);
44    // Need to have useCapture=true, Polymer.Gestures doesn't offer that.
45    document.addEventListener('tap', this._onCaptureClick.bind(this), true);
46    document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
47    document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
48  };
49
50  Polymer.IronOverlayManagerClass.prototype = {
51
52    constructor: Polymer.IronOverlayManagerClass,
53
54    /**
55     * The shared backdrop element.
56     * @type {!Element} backdropElement
57     */
58    get backdropElement() {
59      if (!this._backdropElement) {
60        this._backdropElement = document.createElement('iron-overlay-backdrop');
61      }
62      return this._backdropElement;
63    },
64
65    /**
66     * The deepest active element.
67     * @type {!Element} activeElement the active element
68     */
69    get deepActiveElement() {
70      // document.activeElement can be null
71      // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
72      // In case of null, default it to document.body.
73      var active = document.activeElement || document.body;
74      while (active.root && Polymer.dom(active.root).activeElement) {
75        active = Polymer.dom(active.root).activeElement;
76      }
77      return active;
78    },
79
80    /**
81     * Brings the overlay at the specified index to the front.
82     * @param {number} i
83     * @private
84     */
85    _bringOverlayAtIndexToFront: function(i) {
86      var overlay = this._overlays[i];
87      if (!overlay) {
88        return;
89      }
90      var lastI = this._overlays.length - 1;
91      var currentOverlay = this._overlays[lastI];
92      // Ensure always-on-top overlay stays on top.
93      if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
94        lastI--;
95      }
96      // If already the top element, return.
97      if (i >= lastI) {
98        return;
99      }
100      // Update z-index to be on top.
101      var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
102      if (this._getZ(overlay) <= minimumZ) {
103        this._applyOverlayZ(overlay, minimumZ);
104      }
105
106      // Shift other overlays behind the new on top.
107      while (i < lastI) {
108        this._overlays[i] = this._overlays[i + 1];
109        i++;
110      }
111      this._overlays[lastI] = overlay;
112    },
113
114    /**
115     * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
116     * Also updates the backdrop z-index.
117     * @param {!Element} overlay
118     */
119    addOrRemoveOverlay: function(overlay) {
120      if (overlay.opened) {
121        this.addOverlay(overlay);
122      } else {
123        this.removeOverlay(overlay);
124      }
125    },
126
127    /**
128     * Tracks overlays for z-index and focus management.
129     * Ensures the last added overlay with always-on-top remains on top.
130     * @param {!Element} overlay
131     */
132    addOverlay: function(overlay) {
133      var i = this._overlays.indexOf(overlay);
134      if (i >= 0) {
135        this._bringOverlayAtIndexToFront(i);
136        this.trackBackdrop();
137        return;
138      }
139      var insertionIndex = this._overlays.length;
140      var currentOverlay = this._overlays[insertionIndex - 1];
141      var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
142      var newZ = this._getZ(overlay);
143
144      // Ensure always-on-top overlay stays on top.
145      if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
146        // This bumps the z-index of +2.
147        this._applyOverlayZ(currentOverlay, minimumZ);
148        insertionIndex--;
149        // Update minimumZ to match previous overlay's z-index.
150        var previousOverlay = this._overlays[insertionIndex - 1];
151        minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
152      }
153
154      // Update z-index and insert overlay.
155      if (newZ <= minimumZ) {
156        this._applyOverlayZ(overlay, minimumZ);
157      }
158      this._overlays.splice(insertionIndex, 0, overlay);
159
160      // Get focused node.
161      var element = this.deepActiveElement;
162      overlay.restoreFocusNode = this._overlayParent(element) ? null : element;
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      var node = overlay.restoreFocusOnClose ? overlay.restoreFocusNode : null;
177      overlay.restoreFocusNode = null;
178      // Focus back only if still contained in document.body
179      if (node && Polymer.dom(document.body).deepContains(node)) {
180        node.focus();
181      }
182      this.trackBackdrop();
183    },
184
185    /**
186     * Returns the current overlay.
187     * @return {Element|undefined}
188     */
189    currentOverlay: function() {
190      var i = this._overlays.length - 1;
191      return this._overlays[i];
192    },
193
194    /**
195     * Returns the current overlay z-index.
196     * @return {number}
197     */
198    currentOverlayZ: function() {
199      return this._getZ(this.currentOverlay());
200    },
201
202    /**
203     * Ensures that the minimum z-index of new overlays is at least `minimumZ`.
204     * This does not effect the z-index of any existing overlays.
205     * @param {number} minimumZ
206     */
207    ensureMinimumZ: function(minimumZ) {
208      this._minimumZ = Math.max(this._minimumZ, minimumZ);
209    },
210
211    focusOverlay: function() {
212      var current = /** @type {?} */ (this.currentOverlay());
213      // We have to be careful to focus the next overlay _after_ any current
214      // transitions are complete (due to the state being toggled prior to the
215      // transition). Otherwise, we risk infinite recursion when a transitioning
216      // (closed) overlay becomes the current overlay.
217      //
218      // NOTE: We make the assumption that any overlay that completes a transition
219      // will call into focusOverlay to kick the process back off. Currently:
220      // transitionend -> _applyFocus -> focusOverlay.
221      if (current && !current.transitioning) {
222        current._applyFocus();
223      }
224    },
225
226    /**
227     * Updates the backdrop z-index.
228     */
229    trackBackdrop: function() {
230      var overlay = this._overlayWithBackdrop();
231      // Avoid creating the backdrop if there is no overlay with backdrop.
232      if (!overlay && !this._backdropElement) {
233        return;
234      }
235      this.backdropElement.style.zIndex = this._getZ(overlay) - 1;
236      this.backdropElement.opened = !!overlay;
237    },
238
239    /**
240     * @return {Array<Element>}
241     */
242    getBackdrops: function() {
243      var backdrops = [];
244      for (var i = 0; i < this._overlays.length; i++) {
245        if (this._overlays[i].withBackdrop) {
246          backdrops.push(this._overlays[i]);
247        }
248      }
249      return backdrops;
250    },
251
252    /**
253     * Returns the z-index for the backdrop.
254     * @return {number}
255     */
256    backdropZ: function() {
257      return this._getZ(this._overlayWithBackdrop()) - 1;
258    },
259
260    /**
261     * Returns the first opened overlay that has a backdrop.
262     * @return {Element|undefined}
263     * @private
264     */
265    _overlayWithBackdrop: function() {
266      for (var i = 0; i < this._overlays.length; i++) {
267        if (this._overlays[i].withBackdrop) {
268          return this._overlays[i];
269        }
270      }
271    },
272
273    /**
274     * Calculates the minimum z-index for the overlay.
275     * @param {Element=} overlay
276     * @private
277     */
278    _getZ: function(overlay) {
279      var z = this._minimumZ;
280      if (overlay) {
281        var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
282        // Check if is a number
283        // Number.isNaN not supported in IE 10+
284        if (z1 === z1) {
285          z = z1;
286        }
287      }
288      return z;
289    },
290
291    /**
292     * @param {!Element} element
293     * @param {number|string} z
294     * @private
295     */
296    _setZ: function(element, z) {
297      element.style.zIndex = z;
298    },
299
300    /**
301     * @param {!Element} overlay
302     * @param {number} aboveZ
303     * @private
304     */
305    _applyOverlayZ: function(overlay, aboveZ) {
306      this._setZ(overlay, aboveZ + 2);
307    },
308
309    /**
310     * Returns the overlay containing the provided node. If the node is an overlay,
311     * it returns the node.
312     * @param {Element=} node
313     * @return {Element|undefined}
314     * @private
315     */
316    _overlayParent: function(node) {
317      while (node && node !== document.body) {
318        // Check if it is an overlay.
319        if (node._manager === this) {
320          return node;
321        }
322        // Use logical parentNode, or native ShadowRoot host.
323        node = Polymer.dom(node).parentNode || node.host;
324      }
325    },
326
327    /**
328     * Returns the deepest overlay in the path.
329     * @param {Array<Element>=} path
330     * @return {Element|undefined}
331     * @suppress {missingProperties}
332     * @private
333     */
334    _overlayInPath: function(path) {
335      path = path || [];
336      for (var i = 0; i < path.length; i++) {
337        if (path[i]._manager === this) {
338          return path[i];
339        }
340      }
341    },
342
343    /**
344     * Ensures the click event is delegated to the right overlay.
345     * @param {!Event} event
346     * @private
347     */
348    _onCaptureClick: function(event) {
349      var overlay = /** @type {?} */ (this.currentOverlay());
350      // Check if clicked outside of top overlay.
351      if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
352        overlay._onCaptureClick(event);
353      }
354    },
355
356    /**
357     * Ensures the focus event is delegated to the right overlay.
358     * @param {!Event} event
359     * @private
360     */
361    _onCaptureFocus: function(event) {
362      var overlay = /** @type {?} */ (this.currentOverlay());
363      if (overlay) {
364        overlay._onCaptureFocus(event);
365      }
366    },
367
368    /**
369     * Ensures TAB and ESC keyboard events are delegated to the right overlay.
370     * @param {!Event} event
371     * @private
372     */
373    _onCaptureKeyDown: function(event) {
374      var overlay = /** @type {?} */ (this.currentOverlay());
375      if (overlay) {
376        if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
377          overlay._onCaptureEsc(event);
378        } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
379          overlay._onCaptureTab(event);
380        }
381      }
382    },
383
384    /**
385     * Returns if the overlay1 should be behind overlay2.
386     * @param {!Element} overlay1
387     * @param {!Element} overlay2
388     * @return {boolean}
389     * @suppress {missingProperties}
390     * @private
391     */
392    _shouldBeBehindOverlay: function(overlay1, overlay2) {
393      return !overlay1.alwaysOnTop && overlay2.alwaysOnTop;
394    }
395  };
396
397  Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();
398</script>
399