// 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. 'use strict'; /** * @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('ui.overlay'); base.require('base.properties'); base.require('base.events'); base.require('ui'); base.exportTo('ui', 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 = ui.define('div'); OverlayRoot.prototype = { __proto__: HTMLDivElement.prototype, decorate: function() { this.classList.add('overlay-root'); this.createToolBar_(); 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.onKeydown_ = this.onKeydown_.bind(this); this.onFocusIn_ = this.onFocusIn_.bind(this); this.addEventListener('mousedown', this.onMousedown_.bind(this)); }, toggleToolbar: function(show) { if (show) { if (this.contentHost.firstChild) this.contentHost.insertBefore(this.contentHost.firstChild, this.toolbar_); else this.contentHost.appendChild(this.toolbar_); } else { if (this.toolbar_.parentElement) this.contentHost.removeChild(this.toolbar_); } }, createToolBar_: function() { this.toolbar_ = this.ownerDocument.createElement('div'); this.toolbar_.className = 'tool-bar'; this.exitButton_ = this.ownerDocument.createElement('span'); this.exitButton_.className = 'exit-button'; this.exitButton_.textContent = 'x'; this.exitButton_.title = 'Close Overlay (esc)'; this.toolbar_.appendChild(this.exitButton_); }, /** * 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'); // 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.onFocusIn_, true); overlay.addEventListener('keydown', this.onKeydown_); }, /** * 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 && // tab 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.onKeydown_); this.ownerDocument.removeEventListener('focusin', this.onFocusIn_); } }; /** * Creates a new overlay element. It will not be visible until shown. * @param {Object=} opt_propertyBag Optional properties. * @constructor * @extends {HTMLDivElement} */ var Overlay = 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'); ui.decorate(overlayRoot, OverlayRoot); this.ownerDocument.body.appendChild(overlayRoot); } this.classList.add('overlay'); this.visible_ = false; this.obeyCloseEvents = false; this.additionalCloseKeyCodes = []; this.onKeyDown = this.onKeyDown.bind(this); this.onKeyPress = this.onKeyPress.bind(this); this.onDocumentClick = this.onDocumentClick.bind(this); this.addEventListener('visibleChange', Overlay.prototype.onVisibleChange_.bind(this), true); this.obeyCloseEvents = true; }, get visible() { return this.visible_; }, set visible(newValue) { base.setPropertyAndDispatchChange(this, 'visible', newValue); }, get obeyCloseEvents() { return this.obeyCloseEvents_; }, set obeyCloseEvents(newValue) { base.setPropertyAndDispatchChange(this, 'obeyCloseEvents', newValue); var overlayRoot = this.ownerDocument.querySelector('.overlay-root'); // Currently the toolbar only has the close button. overlayRoot.toggleToolbar(newValue); }, get toolbar() { return this.ownerDocument.querySelector('.overlay-root .tool-bar'); }, onVisibleChange_: function() { var overlayRoot = this.ownerDocument.querySelector('.overlay-root'); if (this.visible) { overlayRoot.setAttribute('visible', 'visible'); overlayRoot.showOverlay(this); document.addEventListener('keydown', this.onKeyDown, true); document.addEventListener('keypress', this.onKeyPress, true); document.addEventListener('click', this.onDocumentClick, true); } else { overlayRoot.removeAttribute('visible'); 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.obeyCloseEvents) return; if (e.keyCode == 27) { // escape this.visible = false; e.preventDefault(); return; } }, onKeyPress: function(e) { if (!this.obeyCloseEvents) 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.obeyCloseEvents) return; var target = e.target; while (target !== null) { if (target === this) return; target = target.parentNode; } this.visible = false; e.preventDefault(); return; } }; return { Overlay: Overlay }; });