| <!-- |
| @license |
| Copyright (c) 2015 The Polymer Project Authors. All rights reserved. |
| This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt |
| The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt |
| The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt |
| Code distributed by Google as part of the polymer project is also |
| subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt |
| --> |
| |
| <link rel="import" href="../polymer/polymer.html"> |
| <link rel="import" href="../iron-a11y-keys-behavior/iron-a11y-keys-behavior.html"> |
| <link rel="import" href="iron-overlay-backdrop.html"> |
| |
| <script> |
| |
| /** |
| * @struct |
| * @constructor |
| * @private |
| */ |
| Polymer.IronOverlayManagerClass = function() { |
| /** |
| * Used to keep track of the opened overlays. |
| * @private {Array<Element>} |
| */ |
| this._overlays = []; |
| |
| /** |
| * iframes have a default z-index of 100, |
| * so this default should be at least that. |
| * @private {number} |
| */ |
| this._minimumZ = 101; |
| |
| /** |
| * Memoized backdrop element. |
| * @private {Element|null} |
| */ |
| this._backdropElement = null; |
| |
| // Enable document-wide tap recognizer. |
| // NOTE: Use useCapture=true to avoid accidentally prevention of the closing |
| // of an overlay via event.stopPropagation(). The only way to prevent |
| // closing of an overlay should be through its APIs. |
| // NOTE: enable tap on <html> to workaround Polymer/polymer#4459 |
| Polymer.Gestures.add(document.documentElement, 'tap', null); |
| document.addEventListener('tap', this._onCaptureClick.bind(this), true); |
| document.addEventListener('focus', this._onCaptureFocus.bind(this), true); |
| document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true); |
| }; |
| |
| Polymer.IronOverlayManagerClass.prototype = { |
| |
| constructor: Polymer.IronOverlayManagerClass, |
| |
| /** |
| * The shared backdrop element. |
| * @type {!Element} backdropElement |
| */ |
| get backdropElement() { |
| if (!this._backdropElement) { |
| this._backdropElement = document.createElement('iron-overlay-backdrop'); |
| } |
| return this._backdropElement; |
| }, |
| |
| /** |
| * The deepest active element. |
| * @type {!Element} activeElement the active element |
| */ |
| get deepActiveElement() { |
| // document.activeElement can be null |
| // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement |
| // In case of null, default it to document.body. |
| var active = document.activeElement || document.body; |
| while (active.root && Polymer.dom(active.root).activeElement) { |
| active = Polymer.dom(active.root).activeElement; |
| } |
| return active; |
| }, |
| |
| /** |
| * Brings the overlay at the specified index to the front. |
| * @param {number} i |
| * @private |
| */ |
| _bringOverlayAtIndexToFront: function(i) { |
| var overlay = this._overlays[i]; |
| if (!overlay) { |
| return; |
| } |
| var lastI = this._overlays.length - 1; |
| var currentOverlay = this._overlays[lastI]; |
| // Ensure always-on-top overlay stays on top. |
| if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) { |
| lastI--; |
| } |
| // If already the top element, return. |
| if (i >= lastI) { |
| return; |
| } |
| // Update z-index to be on top. |
| var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); |
| if (this._getZ(overlay) <= minimumZ) { |
| this._applyOverlayZ(overlay, minimumZ); |
| } |
| |
| // Shift other overlays behind the new on top. |
| while (i < lastI) { |
| this._overlays[i] = this._overlays[i + 1]; |
| i++; |
| } |
| this._overlays[lastI] = overlay; |
| }, |
| |
| /** |
| * Adds the overlay and updates its z-index if it's opened, or removes it if it's closed. |
| * Also updates the backdrop z-index. |
| * @param {!Element} overlay |
| */ |
| addOrRemoveOverlay: function(overlay) { |
| if (overlay.opened) { |
| this.addOverlay(overlay); |
| } else { |
| this.removeOverlay(overlay); |
| } |
| }, |
| |
| /** |
| * Tracks overlays for z-index and focus management. |
| * Ensures the last added overlay with always-on-top remains on top. |
| * @param {!Element} overlay |
| */ |
| addOverlay: function(overlay) { |
| var i = this._overlays.indexOf(overlay); |
| if (i >= 0) { |
| this._bringOverlayAtIndexToFront(i); |
| this.trackBackdrop(); |
| return; |
| } |
| var insertionIndex = this._overlays.length; |
| var currentOverlay = this._overlays[insertionIndex - 1]; |
| var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); |
| var newZ = this._getZ(overlay); |
| |
| // Ensure always-on-top overlay stays on top. |
| if (currentOverlay && this._shouldBeBehindOverlay(overlay, currentOverlay)) { |
| // This bumps the z-index of +2. |
| this._applyOverlayZ(currentOverlay, minimumZ); |
| insertionIndex--; |
| // Update minimumZ to match previous overlay's z-index. |
| var previousOverlay = this._overlays[insertionIndex - 1]; |
| minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); |
| } |
| |
| // Update z-index and insert overlay. |
| if (newZ <= minimumZ) { |
| this._applyOverlayZ(overlay, minimumZ); |
| } |
| this._overlays.splice(insertionIndex, 0, overlay); |
| |
| this.trackBackdrop(); |
| }, |
| |
| /** |
| * @param {!Element} overlay |
| */ |
| removeOverlay: function(overlay) { |
| var i = this._overlays.indexOf(overlay); |
| if (i === -1) { |
| return; |
| } |
| this._overlays.splice(i, 1); |
| |
| this.trackBackdrop(); |
| }, |
| |
| /** |
| * Returns the current overlay. |
| * @return {Element|undefined} |
| */ |
| currentOverlay: function() { |
| var i = this._overlays.length - 1; |
| return this._overlays[i]; |
| }, |
| |
| /** |
| * Returns the current overlay z-index. |
| * @return {number} |
| */ |
| currentOverlayZ: function() { |
| return this._getZ(this.currentOverlay()); |
| }, |
| |
| /** |
| * Ensures that the minimum z-index of new overlays is at least `minimumZ`. |
| * This does not effect the z-index of any existing overlays. |
| * @param {number} minimumZ |
| */ |
| ensureMinimumZ: function(minimumZ) { |
| this._minimumZ = Math.max(this._minimumZ, minimumZ); |
| }, |
| |
| focusOverlay: function() { |
| var current = /** @type {?} */ (this.currentOverlay()); |
| if (current) { |
| current._applyFocus(); |
| } |
| }, |
| |
| /** |
| * Updates the backdrop z-index. |
| */ |
| trackBackdrop: function() { |
| var overlay = this._overlayWithBackdrop(); |
| // Avoid creating the backdrop if there is no overlay with backdrop. |
| if (!overlay && !this._backdropElement) { |
| return; |
| } |
| this.backdropElement.style.zIndex = this._getZ(overlay) - 1; |
| this.backdropElement.opened = !!overlay; |
| }, |
| |
| /** |
| * @return {Array<Element>} |
| */ |
| getBackdrops: function() { |
| var backdrops = []; |
| for (var i = 0; i < this._overlays.length; i++) { |
| if (this._overlays[i].withBackdrop) { |
| backdrops.push(this._overlays[i]); |
| } |
| } |
| return backdrops; |
| }, |
| |
| /** |
| * Returns the z-index for the backdrop. |
| * @return {number} |
| */ |
| backdropZ: function() { |
| return this._getZ(this._overlayWithBackdrop()) - 1; |
| }, |
| |
| /** |
| * Returns the first opened overlay that has a backdrop. |
| * @return {Element|undefined} |
| * @private |
| */ |
| _overlayWithBackdrop: function() { |
| for (var i = 0; i < this._overlays.length; i++) { |
| if (this._overlays[i].withBackdrop) { |
| return this._overlays[i]; |
| } |
| } |
| }, |
| |
| /** |
| * Calculates the minimum z-index for the overlay. |
| * @param {Element=} overlay |
| * @private |
| */ |
| _getZ: function(overlay) { |
| var z = this._minimumZ; |
| if (overlay) { |
| var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay).zIndex); |
| // Check if is a number |
| // Number.isNaN not supported in IE 10+ |
| if (z1 === z1) { |
| z = z1; |
| } |
| } |
| return z; |
| }, |
| |
| /** |
| * @param {!Element} element |
| * @param {number|string} z |
| * @private |
| */ |
| _setZ: function(element, z) { |
| element.style.zIndex = z; |
| }, |
| |
| /** |
| * @param {!Element} overlay |
| * @param {number} aboveZ |
| * @private |
| */ |
| _applyOverlayZ: function(overlay, aboveZ) { |
| this._setZ(overlay, aboveZ + 2); |
| }, |
| |
| /** |
| * Returns the deepest overlay in the path. |
| * @param {Array<Element>=} path |
| * @return {Element|undefined} |
| * @suppress {missingProperties} |
| * @private |
| */ |
| _overlayInPath: function(path) { |
| path = path || []; |
| for (var i = 0; i < path.length; i++) { |
| if (path[i]._manager === this) { |
| return path[i]; |
| } |
| } |
| }, |
| |
| /** |
| * Ensures the click event is delegated to the right overlay. |
| * @param {!Event} event |
| * @private |
| */ |
| _onCaptureClick: function(event) { |
| var overlay = /** @type {?} */ (this.currentOverlay()); |
| // Check if clicked outside of top overlay. |
| if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { |
| overlay._onCaptureClick(event); |
| } |
| }, |
| |
| /** |
| * Ensures the focus event is delegated to the right overlay. |
| * @param {!Event} event |
| * @private |
| */ |
| _onCaptureFocus: function(event) { |
| var overlay = /** @type {?} */ (this.currentOverlay()); |
| if (overlay) { |
| overlay._onCaptureFocus(event); |
| } |
| }, |
| |
| /** |
| * Ensures TAB and ESC keyboard events are delegated to the right overlay. |
| * @param {!Event} event |
| * @private |
| */ |
| _onCaptureKeyDown: function(event) { |
| var overlay = /** @type {?} */ (this.currentOverlay()); |
| if (overlay) { |
| if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc')) { |
| overlay._onCaptureEsc(event); |
| } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'tab')) { |
| overlay._onCaptureTab(event); |
| } |
| } |
| }, |
| |
| /** |
| * Returns if the overlay1 should be behind overlay2. |
| * @param {!Element} overlay1 |
| * @param {!Element} overlay2 |
| * @return {boolean} |
| * @suppress {missingProperties} |
| * @private |
| */ |
| _shouldBeBehindOverlay: function(overlay1, overlay2) { |
| return !overlay1.alwaysOnTop && overlay2.alwaysOnTop; |
| } |
| }; |
| |
| Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); |
| </script> |