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