<!-- @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. Polymer.Gestures.add(document, 'tap', null); // Need to have useCapture=true, Polymer.Gestures doesn't offer that. 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); // Get focused node. var element = this.deepActiveElement; overlay.restoreFocusNode = this._overlayParent(element) ? null : element; this.trackBackdrop(); }, /** * @param {!Element} overlay */ removeOverlay: function(overlay) { var i = this._overlays.indexOf(overlay); if (i === -1) { return; } this._overlays.splice(i, 1); var node = overlay.restoreFocusOnClose ? overlay.restoreFocusNode : null; overlay.restoreFocusNode = null; // Focus back only if still contained in document.body if (node && Polymer.dom(document.body).deepContains(node)) { node.focus(); } 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()); // We have to be careful to focus the next overlay _after_ any current // transitions are complete (due to the state being toggled prior to the // transition). Otherwise, we risk infinite recursion when a transitioning // (closed) overlay becomes the current overlay. // // NOTE: We make the assumption that any overlay that completes a transition // will call into focusOverlay to kick the process back off. Currently: // transitionend -> _applyFocus -> focusOverlay. if (current && !current.transitioning) { 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 overlay containing the provided node. If the node is an overlay, * it returns the node. * @param {Element=} node * @return {Element|undefined} * @private */ _overlayParent: function(node) { while (node && node !== document.body) { // Check if it is an overlay. if (node._manager === this) { return node; } // Use logical parentNode, or native ShadowRoot host. node = Polymer.dom(node).parentNode || node.host; } }, /** * 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>