3
Copyright (c) 2015 The Polymer Project Authors. All rights reserved.
4
This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt
5
The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt
6
The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt
7
Code distributed by Google as part of the polymer project is also
8
subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt
11
<link rel="import" href="../polymer/polymer.html">
12
<link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html">
21
Polymer.IronOverlayManagerClass = function() {
23
* Used to keep track of the opened overlays.
24
* @private {Array<Element>}
29
* iframes have a default z-index of 100,
30
* so this default should be at least that.
36
* Memoized backdrop element.
37
* @private {Element|null}
39
this._backdropElement = null;
41
// Listen to mousedown or touchstart to be sure to be the first to capture
42
// clicks outside the overlay.
43
var clickEvent = ('ontouchstart' in window) ? 'touchstart' : 'mousedown';
44
document.addEventListener(clickEvent, this._onCaptureClick.bind(this), true);
45
document.addEventListener('focus', this._onCaptureFocus.bind(this), true);
46
document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true);
49
Polymer.IronOverlayManagerClass.prototype = {
51
constructor: Polymer.IronOverlayManagerClass,
54
* The shared backdrop element.
55
* @type {!Element} backdropElement
57
get backdropElement() {
58
if (!this._backdropElement) {
59
this._backdropElement = document.createElement('iron-overlay-backdrop');
61
return this._backdropElement;
65
* The deepest active element.
66
* @type {!Element} activeElement the active element
68
get deepActiveElement() {
69
// document.activeElement can be null
70
// https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement
71
// In case of null, default it to document.body.
72
var active = document.activeElement || document.body;
73
while (active.root && Polymer.dom(active.root).activeElement) {
74
active = Polymer.dom(active.root).activeElement;
80
* Brings the overlay at the specified index to the front.
84
_bringOverlayAtIndexToFront: function(i) {
85
var overlay = this._overlays[i];
89
var lastI = this._overlays.length - 1;
90
var currentOverlay = this._overlays[lastI];
91
// Ensure always-on-top overlay stays on top.
92
if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
95
// If already the top element, return.
99
// Update z-index to be on top.
100
var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ);
101
if (this._getZ(overlay) <= minimumZ) {
102
this._applyOverlayZ(overlay, minimumZ);
105
// Shift other overlays behind the new on top.
107
this._overlays[i] = this._overlays[i + 1];
110
this._overlays[lastI] = overlay;
114
* Adds the overlay and updates its z-index if it's opened, or removes it if it's closed.
115
* Also updates the backdrop z-index.
116
* @param {!Element} overlay
118
addOrRemoveOverlay: function(overlay) {
119
if (overlay.opened) {
120
this.addOverlay(overlay);
122
this.removeOverlay(overlay);
124
this.trackBackdrop();
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
132
addOverlay: function(overlay) {
133
var i = this._overlays.indexOf(overlay);
135
this._bringOverlayAtIndexToFront(i);
138
var insertionIndex = this._overlays.length;
139
var currentOverlay = this._overlays[insertionIndex - 1];
140
var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ);
141
var newZ = this._getZ(overlay);
143
// Ensure always-on-top overlay stays on top.
144
if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) {
145
// This bumps the z-index of +2.
146
this._applyOverlayZ(currentOverlay, minimumZ);
148
// Update minimumZ to match previous overlay's z-index.
149
var previousOverlay = this._overlays[insertionIndex - 1];
150
minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ);
153
// Update z-index and insert overlay.
154
if (newZ <= minimumZ) {
155
this._applyOverlayZ(overlay, minimumZ);
157
this._overlays.splice(insertionIndex, 0, overlay);
160
var element = this.deepActiveElement;
161
overlay.restoreFocusNode = this._overlayParent(element) ? null : element;
165
* @param {!Element} overlay
167
removeOverlay: function(overlay) {
168
var i = this._overlays.indexOf(overlay);
172
this._overlays.splice(i, 1);
174
var node = overlay.restoreFocusOnClose ? overlay.restoreFocusNode : null;
175
overlay.restoreFocusNode = null;
176
// Focus back only if still contained in document.body
177
if (node && Polymer.dom(document.body).deepContains(node)) {
183
* Returns the current overlay.
184
* @return {Element|undefined}
186
currentOverlay: function() {
187
var i = this._overlays.length - 1;
188
return this._overlays[i];
192
* Returns the current overlay z-index.
195
currentOverlayZ: function() {
196
return this._getZ(this.currentOverlay());
200
* Ensures that the minimum z-index of new overlays is at least `minimumZ`.
201
* This does not effect the z-index of any existing overlays.
202
* @param {number} minimumZ
204
ensureMinimumZ: function(minimumZ) {
205
this._minimumZ = Math.max(this._minimumZ, minimumZ);
208
focusOverlay: function() {
209
var current = /** @type {?} */ (this.currentOverlay());
210
// We have to be careful to focus the next overlay _after_ any current
211
// transitions are complete (due to the state being toggled prior to the
212
// transition). Otherwise, we risk infinite recursion when a transitioning
213
// (closed) overlay becomes the current overlay.
215
// NOTE: We make the assumption that any overlay that completes a transition
216
// will call into focusOverlay to kick the process back off. Currently:
217
// transitionend -> _applyFocus -> focusOverlay.
218
if (current && !current.transitioning) {
219
current._applyFocus();
224
* Updates the backdrop z-index.
226
trackBackdrop: function() {
227
this.backdropElement.style.zIndex = this.backdropZ();
231
* @return {Array<Element>}
233
getBackdrops: function() {
235
for (var i = 0; i < this._overlays.length; i++) {
236
if (this._overlays[i].withBackdrop) {
237
backdrops.push(this._overlays[i]);
244
* Returns the z-index for the backdrop.
247
backdropZ: function() {
248
return this._getZ(this._overlayWithBackdrop()) - 1;
252
* Returns the first opened overlay that has a backdrop.
253
* @return {Element|undefined}
256
_overlayWithBackdrop: function() {
257
for (var i = 0; i < this._overlays.length; i++) {
258
if (this._overlays[i].withBackdrop) {
259
return this._overlays[i];
265
* Calculates the minimum z-index for the overlay.
266
* @param {Element=} overlay
269
_getZ: function(overlay) {
270
var z = this._minimumZ;
272
var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex);
273
// Check if is a number
274
// Number.isNaN not supported in IE 10+
283
* @param {!Element} element
284
* @param {number|string} z
287
_setZ: function(element, z) {
288
element.style.zIndex = z;
292
* @param {!Element} overlay
293
* @param {number} aboveZ
296
_applyOverlayZ: function(overlay, aboveZ) {
297
this._setZ(overlay, aboveZ + 2);
301
* Returns the overlay containing the provided node. If the node is an overlay,
302
* it returns the node.
303
* @param {Element=} node
304
* @return {Element|undefined}
307
_overlayParent: function(node) {
308
while (node && node !== document.body) {
309
// Check if it is an overlay.
310
if (node._manager === this) {
313
// Use logical parentNode, or native ShadowRoot host.
314
node = Polymer.dom(node).parentNode || node.host;
319
* Returns the deepest overlay in the path.
320
* @param {Array<Element>=} path
321
* @return {Element|undefined}
324
_overlayInPath: function(path) {
326
for (var i = 0; i < path.length; i++) {
327
if (path[i]._manager === this) {
334
* Ensures the click event is delegated to the right overlay.
335
* @param {!Event} event
338
_onCaptureClick: function(event) {
339
var overlay = /** @type {?} */ (this.currentOverlay());
340
// Check if clicked outside of top overlay.
341
if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) {
342
overlay._onCaptureClick(event);
347
* Ensures the focus event is delegated to the right overlay.
348
* @param {!Event} event
351
_onCaptureFocus: function(event) {
352
var overlay = /** @type {?} */ (this.currentOverlay());
354
overlay._onCaptureFocus(event);
359
* Ensures TAB and ESC keyboard events are delegated to the right overlay.
360
* @param {!Event} event
363
_onCaptureKeyDown: function(event) {
364
var overlay = /** @type {?} */ (this.currentOverlay());
366
if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) {
367
overlay._onCaptureEsc(event);
368
} else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) {
369
overlay._onCaptureTab(event);
375
* Returns if the overlay1 should be behind overlay2.
376
* @param {!Element} overlay1
377
* @param {!Element} overlay2
381
_shouldBeBehindOverlay: function(overlay1, overlay2) {
382
var o1 = /** @type {?} */ (overlay1);
383
var o2 = /** @type {?} */ (overlay2);
384
return !o1.alwaysOnTop && o2.alwaysOnTop;
388
Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass();