1 <!-- 2 @license 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 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