| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| |
| /** |
| * @fileoverview Implements an element that is hidden by default, but |
| * when shown, dims and (attempts to) disable the main document. |
| * |
| * You can turn any div into an overlay. Note that while an |
| * overlay element is shown, its parent is changed. Hiding the overlay |
| * restores its original parentage. |
| * |
| */ |
| base.requireStylesheet('overlay'); |
| base.require('ui'); |
| base.require('event_target'); |
| base.exportTo('tracing', function() { |
| /** |
| * Manages a full-window div that darkens the window, disables |
| * input, and hosts the currently-visible overlays. You shouldn't |
| * have to instantiate this directly --- it gets set automatically. |
| * @param {Object=} opt_propertyBag Optional properties. |
| * @constructor |
| * @extends {HTMLDivElement} |
| */ |
| var OverlayRoot = base.ui.define('div'); |
| OverlayRoot.prototype = { |
| __proto__: HTMLDivElement.prototype, |
| decorate: function() { |
| this.classList.add('overlay-root'); |
| this.visible = false; |
| |
| this.contentHost = this.ownerDocument.createElement('div'); |
| this.contentHost.classList.add('content-host'); |
| |
| this.tabCatcher = this.ownerDocument.createElement('span'); |
| this.tabCatcher.tabIndex = 0; |
| |
| this.appendChild(this.contentHost); |
| |
| this.onKeydownBoundToThis_ = this.onKeydown_.bind(this); |
| this.onFocusInBoundToThis_ = this.onFocusIn_.bind(this); |
| this.addEventListener('mousedown', this.onMousedown_.bind(this)); |
| }, |
| |
| /** |
| * Adds an overlay, attaching it to the contentHost so that it is visible. |
| */ |
| showOverlay: function(overlay) { |
| // Reparent this to the overlay content host. |
| overlay.oldParent_ = overlay.parentNode; |
| this.contentHost.appendChild(overlay); |
| this.contentHost.appendChild(this.tabCatcher); |
| |
| // Show the overlay root. |
| this.ownerDocument.body.classList.add('disabled-by-overlay'); |
| this.visible = true; |
| |
| // Bring overlay into focus. |
| overlay.tabIndex = 0; |
| var focusElement = |
| overlay.querySelector('button, input, list, select, a'); |
| if (!focusElement) { |
| focusElement = overlay; |
| } |
| focusElement.focus(); |
| |
| // Listen to key and focus events to prevent focus from |
| // leaving the overlay. |
| this.ownerDocument.addEventListener('focusin', |
| this.onFocusInBoundToThis_, true); |
| overlay.addEventListener('keydown', this.onKeydownBoundToThis_); |
| }, |
| |
| /** |
| * Clicking outside of the overlay will de-focus the overlay. The |
| * next tab will look at the entire document to determine the focus. |
| * For certain documents, this can cause focus to "leak" outside of |
| * the overlay. |
| */ |
| onMousedown_: function(e) { |
| if (e.target == this) { |
| e.preventDefault(); |
| } |
| }, |
| |
| /** |
| * Prevents forward-tabbing out of the overlay |
| */ |
| onFocusIn_: function(e) { |
| if (e.target == this.tabCatcher) { |
| window.setTimeout(this.focusOverlay_.bind(this), 0); |
| } |
| }, |
| |
| focusOverlay_: function() { |
| this.contentHost.firstChild.focus(); |
| }, |
| |
| /** |
| * Prevent the user from shift-tabbing backwards out of the overlay. |
| */ |
| onKeydown_: function(e) { |
| if (e.keyCode == 9 && |
| e.shiftKey && |
| e.target == this.contentHost.firstChild) { |
| e.preventDefault(); |
| } |
| }, |
| |
| /** |
| * Hides an overlay, attaching it to its original parent if needed. |
| */ |
| hideOverlay: function(overlay) { |
| // hide the overlay root |
| this.visible = false; |
| this.ownerDocument.body.classList.remove('disabled-by-overlay'); |
| this.lastFocusOut_ = undefined; |
| |
| // put the overlay back on its previous parent |
| overlay.parentNode.removeChild(this.tabCatcher); |
| if (overlay.oldParent_) { |
| overlay.oldParent_.appendChild(overlay); |
| delete overlay.oldParent_; |
| } else { |
| this.contentHost.removeChild(overlay); |
| } |
| |
| // remove listeners |
| overlay.removeEventListener('keydown', this.onKeydownBoundToThis_); |
| this.ownerDocument.removeEventListener('focusin', |
| this.onFocusInBoundToThis_); |
| } |
| }; |
| |
| base.defineProperty(OverlayRoot, 'visible', base.PropertyKind.BOOL_ATTR); |
| |
| /** |
| * Creates a new overlay element. It will not be visible until shown. |
| * @param {Object=} opt_propertyBag Optional properties. |
| * @constructor |
| * @extends {HTMLDivElement} |
| */ |
| var Overlay = base.ui.define('div'); |
| |
| Overlay.prototype = { |
| __proto__: HTMLDivElement.prototype, |
| |
| /** |
| * Initializes the overlay element. |
| */ |
| decorate: function() { |
| // create the overlay root on this document if its not present |
| if (!this.ownerDocument.querySelector('.overlay-root')) { |
| var overlayRoot = this.ownerDocument.createElement('div'); |
| base.ui.decorate(overlayRoot, OverlayRoot); |
| this.ownerDocument.body.appendChild(overlayRoot); |
| } |
| |
| this.classList.add('overlay'); |
| this.visible = false; |
| this.defaultClickShouldClose = true; |
| this.autoClose = false; |
| this.additionalCloseKeyCodes = []; |
| this.onKeyDown = this.onKeyDown.bind(this); |
| this.onKeyPress = this.onKeyPress.bind(this); |
| this.onDocumentClick = this.onDocumentClick.bind(this); |
| }, |
| |
| onVisibleChanged_: function() { |
| var overlayRoot = this.ownerDocument.querySelector('.overlay-root'); |
| base.dispatchSimpleEvent(this, 'visibleChange'); |
| if (this.visible) { |
| overlayRoot.showOverlay(this); |
| document.addEventListener('keydown', this.onKeyDown, true); |
| document.addEventListener('keypress', this.onKeyPress, true); |
| document.addEventListener('click', this.onDocumentClick, true); |
| } else { |
| document.removeEventListener('keydown', this.onKeyDown, true); |
| document.removeEventListener('keypress', this.onKeyPress, true); |
| document.removeEventListener('click', this.onDocumentClick, true); |
| overlayRoot.hideOverlay(this); |
| } |
| }, |
| |
| onKeyDown: function(e) { |
| if (!this.autoClose) |
| return; |
| |
| if (e.keyCode == 27) { |
| this.visible = false; |
| e.preventDefault(); |
| return; |
| } |
| }, |
| |
| onKeyPress: function(e) { |
| if (!this.autoClose) |
| return; |
| |
| for (var i = 0; i < this.additionalCloseKeyCodes.length; i++) { |
| if (e.keyCode == this.additionalCloseKeyCodes[i]) { |
| this.visible = false; |
| e.preventDefault(); |
| return; |
| } |
| } |
| }, |
| |
| onDocumentClick: function(e) { |
| if (!this.defaultClickShouldClose) |
| return; |
| var target = e.target; |
| while (target !== null) { |
| if (target === this) |
| return; |
| target = target.parentNode; |
| } |
| this.visible = false; |
| e.preventDefault(); |
| return; |
| } |
| |
| }; |
| |
| /** |
| * Shows and hides the overlay. Note that while visible == true, the overlay |
| * element will be tempoarily reparented to another place in the DOM. |
| */ |
| base.defineProperty(Overlay, 'visible', base.PropertyKind.BOOL_ATTR, |
| Overlay.prototype.onVisibleChanged_); |
| base.defineProperty(Overlay, 'defaultClickShouldClose', |
| base.PropertyKind.BOOL_ATTR); |
| |
| return { |
| Overlay: Overlay |
| }; |
| }); |