| OLD | NEW |
| 1 /** | 1 /** |
| 2 * @struct | 2 * @struct |
| 3 * @constructor | 3 * @constructor |
| 4 * @private |
| 4 */ | 5 */ |
| 5 Polymer.IronOverlayManagerClass = function() { | 6 Polymer.IronOverlayManagerClass = function() { |
| 7 /** |
| 8 * Used to keep track of the opened overlays. |
| 9 * @private {Array<Element>} |
| 10 */ |
| 6 this._overlays = []; | 11 this._overlays = []; |
| 7 // Used to keep track of the last focused node before an overlay gets opened
. | 12 |
| 8 this._lastFocusedNodes = []; | 13 /** |
| 9 | 14 * iframes have a default z-index of 100, |
| 10 /** | 15 * so this default should be at least that. |
| 11 * iframes have a default z-index of 100, so this default should be at least | |
| 12 * that. | |
| 13 * @private {number} | 16 * @private {number} |
| 14 */ | 17 */ |
| 15 this._minimumZ = 101; | 18 this._minimumZ = 101; |
| 16 | 19 |
| 17 this._backdrops = []; | 20 /** |
| 18 | 21 * Memoized backdrop element. |
| 22 * @private {Element|null} |
| 23 */ |
| 19 this._backdropElement = null; | 24 this._backdropElement = null; |
| 20 Object.defineProperty(this, 'backdropElement', { | 25 |
| 21 get: function() { | 26 // Listen to mousedown or touchstart to be sure to be the first to capture |
| 22 if (!this._backdropElement) { | 27 // clicks outside the overlay. |
| 23 this._backdropElement = document.createElement('iron-overlay-backdrop'
); | 28 var clickEvent = ('ontouchstart' in window) ? 'touchstart' : 'mousedown'; |
| 24 } | 29 document.addEventListener(clickEvent, this._onCaptureClick.bind(this), true)
; |
| 25 return this._backdropElement; | 30 document.addEventListener('focus', this._onCaptureFocus.bind(this), true); |
| 26 }.bind(this) | 31 document.addEventListener('keydown', this._onCaptureKeyDown.bind(this), true
); |
| 27 }); | 32 }; |
| 33 |
| 34 Polymer.IronOverlayManagerClass.prototype = { |
| 35 |
| 36 constructor: Polymer.IronOverlayManagerClass, |
| 37 |
| 38 /** |
| 39 * The shared backdrop element. |
| 40 * @type {Element} backdropElement |
| 41 */ |
| 42 get backdropElement() { |
| 43 if (!this._backdropElement) { |
| 44 this._backdropElement = document.createElement('iron-overlay-backdrop'); |
| 45 } |
| 46 return this._backdropElement; |
| 47 }, |
| 28 | 48 |
| 29 /** | 49 /** |
| 30 * The deepest active element. | 50 * The deepest active element. |
| 31 * returns {?Node} element the active element | 51 * @type {Element} activeElement the active element |
| 32 */ | 52 */ |
| 33 this.deepActiveElement = null; | 53 get deepActiveElement() { |
| 34 Object.defineProperty(this, 'deepActiveElement', { | 54 // document.activeElement can be null |
| 35 get: function() { | 55 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeElement |
| 36 var active = document.activeElement; | 56 // In case of null, default it to document.body. |
| 37 // document.activeElement can be null | 57 var active = document.activeElement || document.body; |
| 38 // https://developer.mozilla.org/en-US/docs/Web/API/Document/activeEleme
nt | 58 while (active.root && Polymer.dom(active.root).activeElement) { |
| 39 while (active && active.root && Polymer.dom(active.root).activeElement)
{ | 59 active = Polymer.dom(active.root).activeElement; |
| 40 active = Polymer.dom(active.root).activeElement; | 60 } |
| 41 } | 61 return active; |
| 42 return active; | 62 }, |
| 43 }.bind(this) | 63 |
| 44 }); | 64 /** |
| 45 }; | 65 * Brings the overlay at the specified index to the front. |
| 46 | 66 * @param {number} i |
| 47 /** | 67 * @private |
| 48 * If a node is contained in an overlay. | 68 */ |
| 49 * @private | 69 _bringOverlayAtIndexToFront: function(i) { |
| 50 * @param {Node} node | 70 var overlay = this._overlays[i]; |
| 51 * @returns {Boolean} | 71 var lastI = this._overlays.length - 1; |
| 52 */ | 72 // Ensure always-on-top overlay stays on top. |
| 53 Polymer.IronOverlayManagerClass.prototype._isChildOfOverlay = function(node) { | 73 if (!overlay.alwaysOnTop && this._overlays[lastI].alwaysOnTop) { |
| 54 while (node && node !== document.body) { | 74 lastI--; |
| 55 // Use logical parentNode, or native ShadowRoot host. | 75 } |
| 56 node = Polymer.dom(node).parentNode || node.host; | 76 // If already the top element, return. |
| 57 // Check if it is an overlay. | 77 if (!overlay || i >= lastI) { |
| 58 if (node && node.behaviors && node.behaviors.indexOf(Polymer.IronOverlayBe
haviorImpl) !== -1) { | 78 return; |
| 59 return true; | 79 } |
| 60 } | 80 // Update z-index to be on top. |
| 61 } | 81 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); |
| 62 return false; | 82 if (this._getZ(overlay) <= minimumZ) { |
| 63 }; | 83 this._applyOverlayZ(overlay, minimumZ); |
| 64 | 84 } |
| 65 Polymer.IronOverlayManagerClass.prototype._applyOverlayZ = function(overlay, a
boveZ) { | 85 |
| 66 this._setZ(overlay, aboveZ + 2); | 86 // Shift other overlays behind the new on top. |
| 67 }; | 87 while (i < lastI) { |
| 68 | 88 this._overlays[i] = this._overlays[i + 1]; |
| 69 Polymer.IronOverlayManagerClass.prototype._setZ = function(element, z) { | 89 i++; |
| 70 element.style.zIndex = z; | 90 } |
| 71 }; | 91 this._overlays[lastI] = overlay; |
| 72 | 92 }, |
| 73 /** | 93 |
| 74 * track overlays for z-index and focus managemant | 94 /** |
| 75 */ | 95 * Adds the overlay and updates its z-index if it's opened, or removes it if
it's closed. |
| 76 Polymer.IronOverlayManagerClass.prototype.addOverlay = function(overlay) { | 96 * Also updates the backdrop z-index. |
| 77 var minimumZ = Math.max(this.currentOverlayZ(), this._minimumZ); | 97 * @param {Element} overlay |
| 78 this._overlays.push(overlay); | 98 */ |
| 79 var newZ = this.currentOverlayZ(); | 99 addOrRemoveOverlay: function(overlay) { |
| 80 if (newZ <= minimumZ) { | 100 if (overlay.opened) { |
| 81 this._applyOverlayZ(overlay, minimumZ); | 101 this.addOverlay(overlay); |
| 82 } | 102 } else { |
| 83 var element = this.deepActiveElement; | 103 this.removeOverlay(overlay); |
| 84 // If already in other overlay, don't reset focus there. | 104 } |
| 85 if (this._isChildOfOverlay(element)) { | 105 this.trackBackdrop(); |
| 86 element = null; | 106 }, |
| 87 } | 107 |
| 88 this._lastFocusedNodes.push(element); | 108 /** |
| 89 }; | 109 * Tracks overlays for z-index and focus management. |
| 90 | 110 * Ensures the last added overlay with always-on-top remains on top. |
| 91 Polymer.IronOverlayManagerClass.prototype.removeOverlay = function(overlay) { | 111 * @param {Element} overlay |
| 92 var i = this._overlays.indexOf(overlay); | 112 */ |
| 93 if (i >= 0) { | 113 addOverlay: function(overlay) { |
| 114 var i = this._overlays.indexOf(overlay); |
| 115 if (i >= 0) { |
| 116 this._bringOverlayAtIndexToFront(i); |
| 117 return; |
| 118 } |
| 119 var insertionIndex = this._overlays.length; |
| 120 var currentOverlay = this._overlays[insertionIndex - 1]; |
| 121 var minimumZ = Math.max(this._getZ(currentOverlay), this._minimumZ); |
| 122 var newZ = this._getZ(overlay); |
| 123 |
| 124 // Ensure always-on-top overlay stays on top. |
| 125 if (currentOverlay && currentOverlay.alwaysOnTop && !overlay.alwaysOnTop)
{ |
| 126 // This bumps the z-index of +2. |
| 127 this._applyOverlayZ(currentOverlay, minimumZ); |
| 128 insertionIndex--; |
| 129 // Update minimumZ to match previous overlay's z-index. |
| 130 var previousOverlay = this._overlays[insertionIndex - 1]; |
| 131 minimumZ = Math.max(this._getZ(previousOverlay), this._minimumZ); |
| 132 } |
| 133 |
| 134 // Update z-index and insert overlay. |
| 135 if (newZ <= minimumZ) { |
| 136 this._applyOverlayZ(overlay, minimumZ); |
| 137 } |
| 138 this._overlays.splice(insertionIndex, 0, overlay); |
| 139 |
| 140 // Get focused node. |
| 141 var element = this.deepActiveElement; |
| 142 overlay.restoreFocusNode = this._overlayParent(element) ? null : element; |
| 143 }, |
| 144 |
| 145 /** |
| 146 * @param {Element} overlay |
| 147 */ |
| 148 removeOverlay: function(overlay) { |
| 149 var i = this._overlays.indexOf(overlay); |
| 150 if (i === -1) { |
| 151 return; |
| 152 } |
| 94 this._overlays.splice(i, 1); | 153 this._overlays.splice(i, 1); |
| 95 this._setZ(overlay, ''); | 154 |
| 96 | 155 var node = overlay.restoreFocusOnClose ? overlay.restoreFocusNode : null; |
| 97 var node = this._lastFocusedNodes[i]; | 156 overlay.restoreFocusNode = null; |
| 98 // Focus only if still contained in document.body | 157 // Focus back only if still contained in document.body |
| 99 if (overlay.restoreFocusOnClose && node && Polymer.dom(document.body).deep
Contains(node)) { | 158 if (node && Polymer.dom(document.body).deepContains(node)) { |
| 100 node.focus(); | 159 node.focus(); |
| 101 } | 160 } |
| 102 this._lastFocusedNodes.splice(i, 1); | 161 }, |
| 162 |
| 163 /** |
| 164 * Returns the current overlay. |
| 165 * @return {Element|undefined} |
| 166 */ |
| 167 currentOverlay: function() { |
| 168 var i = this._overlays.length - 1; |
| 169 return this._overlays[i]; |
| 170 }, |
| 171 |
| 172 /** |
| 173 * Returns the current overlay z-index. |
| 174 * @return {number} |
| 175 */ |
| 176 currentOverlayZ: function() { |
| 177 return this._getZ(this.currentOverlay()); |
| 178 }, |
| 179 |
| 180 /** |
| 181 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. |
| 182 * This does not effect the z-index of any existing overlays. |
| 183 * @param {number} minimumZ |
| 184 */ |
| 185 ensureMinimumZ: function(minimumZ) { |
| 186 this._minimumZ = Math.max(this._minimumZ, minimumZ); |
| 187 }, |
| 188 |
| 189 focusOverlay: function() { |
| 190 var current = /** @type {?} */ (this.currentOverlay()); |
| 191 // We have to be careful to focus the next overlay _after_ any current |
| 192 // transitions are complete (due to the state being toggled prior to the |
| 193 // transition). Otherwise, we risk infinite recursion when a transitioning |
| 194 // (closed) overlay becomes the current overlay. |
| 195 // |
| 196 // NOTE: We make the assumption that any overlay that completes a transiti
on |
| 197 // will call into focusOverlay to kick the process back off. Currently: |
| 198 // transitionend -> _applyFocus -> focusOverlay. |
| 199 if (current && !current.transitioning) { |
| 200 current._applyFocus(); |
| 201 } |
| 202 }, |
| 203 |
| 204 /** |
| 205 * Updates the backdrop z-index. |
| 206 */ |
| 207 trackBackdrop: function() { |
| 208 this.backdropElement.style.zIndex = this.backdropZ(); |
| 209 }, |
| 210 |
| 211 /** |
| 212 * @return {Array<Element>} |
| 213 */ |
| 214 getBackdrops: function() { |
| 215 var backdrops = []; |
| 216 for (var i = 0; i < this._overlays.length; i++) { |
| 217 if (this._overlays[i].withBackdrop) { |
| 218 backdrops.push(this._overlays[i]); |
| 219 } |
| 220 } |
| 221 return backdrops; |
| 222 }, |
| 223 |
| 224 /** |
| 225 * Returns the z-index for the backdrop. |
| 226 * @return {number} |
| 227 */ |
| 228 backdropZ: function() { |
| 229 return this._getZ(this._overlayWithBackdrop()) - 1; |
| 230 }, |
| 231 |
| 232 /** |
| 233 * Returns the first opened overlay that has a backdrop. |
| 234 * @return {Element|undefined} |
| 235 * @private |
| 236 */ |
| 237 _overlayWithBackdrop: function() { |
| 238 for (var i = 0; i < this._overlays.length; i++) { |
| 239 if (this._overlays[i].withBackdrop) { |
| 240 return this._overlays[i]; |
| 241 } |
| 242 } |
| 243 }, |
| 244 |
| 245 /** |
| 246 * Calculates the minimum z-index for the overlay. |
| 247 * @param {Element=} overlay |
| 248 * @private |
| 249 */ |
| 250 _getZ: function(overlay) { |
| 251 var z = this._minimumZ; |
| 252 if (overlay) { |
| 253 var z1 = Number(overlay.style.zIndex || window.getComputedStyle(overlay)
.zIndex); |
| 254 // Check if is a number |
| 255 // Number.isNaN not supported in IE 10+ |
| 256 if (z1 === z1) { |
| 257 z = z1; |
| 258 } |
| 259 } |
| 260 return z; |
| 261 }, |
| 262 |
| 263 /** |
| 264 * @param {Element} element |
| 265 * @param {number|string} z |
| 266 * @private |
| 267 */ |
| 268 _setZ: function(element, z) { |
| 269 element.style.zIndex = z; |
| 270 }, |
| 271 |
| 272 /** |
| 273 * @param {Element} overlay |
| 274 * @param {number} aboveZ |
| 275 * @private |
| 276 */ |
| 277 _applyOverlayZ: function(overlay, aboveZ) { |
| 278 this._setZ(overlay, aboveZ + 2); |
| 279 }, |
| 280 |
| 281 /** |
| 282 * Returns the overlay containing the provided node. If the node is an overl
ay, |
| 283 * it returns the node. |
| 284 * @param {Element=} node |
| 285 * @return {Element|undefined} |
| 286 * @private |
| 287 */ |
| 288 _overlayParent: function(node) { |
| 289 while (node && node !== document.body) { |
| 290 // Check if it is an overlay. |
| 291 if (node._manager === this) { |
| 292 return node; |
| 293 } |
| 294 // Use logical parentNode, or native ShadowRoot host. |
| 295 node = Polymer.dom(node).parentNode || node.host; |
| 296 } |
| 297 }, |
| 298 |
| 299 /** |
| 300 * Returns the deepest overlay in the path. |
| 301 * @param {Array<Element>=} path |
| 302 * @return {Element|undefined} |
| 303 * @private |
| 304 */ |
| 305 _overlayInPath: function(path) { |
| 306 path = path || []; |
| 307 for (var i = 0; i < path.length; i++) { |
| 308 if (path[i]._manager === this) { |
| 309 return path[i]; |
| 310 } |
| 311 } |
| 312 }, |
| 313 |
| 314 /** |
| 315 * Ensures the click event is delegated to the right overlay. |
| 316 * @param {!Event} event |
| 317 * @private |
| 318 */ |
| 319 _onCaptureClick: function(event) { |
| 320 var overlay = /** @type {?} */ (this.currentOverlay()); |
| 321 // Check if clicked outside of top overlay. |
| 322 if (overlay && this._overlayInPath(Polymer.dom(event).path) !== overlay) { |
| 323 overlay._onCaptureClick(event); |
| 324 } |
| 325 }, |
| 326 |
| 327 /** |
| 328 * Ensures the focus event is delegated to the right overlay. |
| 329 * @param {!Event} event |
| 330 * @private |
| 331 */ |
| 332 _onCaptureFocus: function(event) { |
| 333 var overlay = /** @type {?} */ (this.currentOverlay()); |
| 334 if (overlay) { |
| 335 overlay._onCaptureFocus(event); |
| 336 } |
| 337 }, |
| 338 |
| 339 /** |
| 340 * Ensures TAB and ESC keyboard events are delegated to the right overlay. |
| 341 * @param {!Event} event |
| 342 * @private |
| 343 */ |
| 344 _onCaptureKeyDown: function(event) { |
| 345 var overlay = /** @type {?} */ (this.currentOverlay()); |
| 346 if (overlay) { |
| 347 if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event, 'esc'))
{ |
| 348 overlay._onCaptureEsc(event); |
| 349 } else if (Polymer.IronA11yKeysBehavior.keyboardEventMatchesKeys(event,
'tab')) { |
| 350 overlay._onCaptureTab(event); |
| 351 } |
| 352 } |
| 103 } | 353 } |
| 104 }; | 354 }; |
| 105 | 355 |
| 106 Polymer.IronOverlayManagerClass.prototype.currentOverlay = function() { | |
| 107 var i = this._overlays.length - 1; | |
| 108 while (this._overlays[i] && !this._overlays[i].opened) { | |
| 109 --i; | |
| 110 } | |
| 111 return this._overlays[i]; | |
| 112 }; | |
| 113 | |
| 114 Polymer.IronOverlayManagerClass.prototype.currentOverlayZ = function() { | |
| 115 return this._getOverlayZ(this.currentOverlay()); | |
| 116 }; | |
| 117 | |
| 118 /** | |
| 119 * Ensures that the minimum z-index of new overlays is at least `minimumZ`. | |
| 120 * This does not effect the z-index of any existing overlays. | |
| 121 * | |
| 122 * @param {number} minimumZ | |
| 123 */ | |
| 124 Polymer.IronOverlayManagerClass.prototype.ensureMinimumZ = function(minimumZ)
{ | |
| 125 this._minimumZ = Math.max(this._minimumZ, minimumZ); | |
| 126 }; | |
| 127 | |
| 128 Polymer.IronOverlayManagerClass.prototype.focusOverlay = function() { | |
| 129 var current = this.currentOverlay(); | |
| 130 // We have to be careful to focus the next overlay _after_ any current | |
| 131 // transitions are complete (due to the state being toggled prior to the | |
| 132 // transition). Otherwise, we risk infinite recursion when a transitioning | |
| 133 // (closed) overlay becomes the current overlay. | |
| 134 // | |
| 135 // NOTE: We make the assumption that any overlay that completes a transition | |
| 136 // will call into focusOverlay to kick the process back off. Currently: | |
| 137 // transitionend -> _applyFocus -> focusOverlay. | |
| 138 if (current && !current.transitioning) { | |
| 139 current._applyFocus(); | |
| 140 } | |
| 141 }; | |
| 142 | |
| 143 Polymer.IronOverlayManagerClass.prototype.trackBackdrop = function(element) { | |
| 144 // backdrops contains the overlays with a backdrop that are currently | |
| 145 // visible | |
| 146 var index = this._backdrops.indexOf(element); | |
| 147 if (element.opened && element.withBackdrop) { | |
| 148 // no duplicates | |
| 149 if (index === -1) { | |
| 150 this._backdrops.push(element); | |
| 151 } | |
| 152 } else if (index >= 0) { | |
| 153 this._backdrops.splice(index, 1); | |
| 154 } | |
| 155 }; | |
| 156 | |
| 157 Polymer.IronOverlayManagerClass.prototype.getBackdrops = function() { | |
| 158 return this._backdrops; | |
| 159 }; | |
| 160 | |
| 161 /** | |
| 162 * Returns the z-index for the backdrop. | |
| 163 */ | |
| 164 Polymer.IronOverlayManagerClass.prototype.backdropZ = function() { | |
| 165 return this._getOverlayZ(this._overlayWithBackdrop()) - 1; | |
| 166 }; | |
| 167 | |
| 168 /** | |
| 169 * Returns the first opened overlay that has a backdrop. | |
| 170 */ | |
| 171 Polymer.IronOverlayManagerClass.prototype._overlayWithBackdrop = function() { | |
| 172 for (var i = 0; i < this._overlays.length; i++) { | |
| 173 if (this._overlays[i].opened && this._overlays[i].withBackdrop) { | |
| 174 return this._overlays[i]; | |
| 175 } | |
| 176 } | |
| 177 }; | |
| 178 | |
| 179 /** | |
| 180 * Calculates the minimum z-index for the overlay. | |
| 181 */ | |
| 182 Polymer.IronOverlayManagerClass.prototype._getOverlayZ = function(overlay) { | |
| 183 var z = this._minimumZ; | |
| 184 if (overlay) { | |
| 185 var z1 = Number(window.getComputedStyle(overlay).zIndex); | |
| 186 // Check if is a number | |
| 187 // Number.isNaN not supported in IE 10+ | |
| 188 if (z1 === z1) { | |
| 189 z = z1; | |
| 190 } | |
| 191 } | |
| 192 return z; | |
| 193 }; | |
| 194 | |
| 195 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); | 356 Polymer.IronOverlayManager = new Polymer.IronOverlayManagerClass(); |
| OLD | NEW |