1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 6 /** 7 * @fileoverview Implements an element that is hidden by default, but 8 * when shown, dims and (attempts to) disable the main document. 9 * 10 * You can turn any div into an overlay. Note that while an 11 * overlay element is shown, its parent is changed. Hiding the overlay 12 * restores its original parentage. 13 * 14 */ 15 base.requireStylesheet('overlay'); 16 base.require('ui'); 17 base.require('event_target'); 18 base.exportTo('tracing.ui', function() { 19 /** 20 * Manages a full-window div that darkens the window, disables 21 * input, and hosts the currently-visible overlays. You shouldn't 22 * have to instantiate this directly --- it gets set automatically. 23 * @param {Object=} opt_propertyBag Optional properties. 24 * @constructor 25 * @extends {HTMLDivElement} 26 */ 27 var OverlayRoot = tracing.ui.define('div'); 28 OverlayRoot.prototype = { 29 __proto__: HTMLDivElement.prototype, 30 decorate: function() { 31 this.classList.add('overlay-root'); 32 this.visible = false; 33 34 this.contentHost = this.ownerDocument.createElement('div'); 35 this.contentHost.classList.add('content-host'); 36 37 this.tabCatcher = this.ownerDocument.createElement('span'); 38 this.tabCatcher.tabIndex = 0; 39 40 this.appendChild(this.contentHost); 41 42 this.onKeydownBoundToThis_ = this.onKeydown_.bind(this); 43 this.onFocusInBoundToThis_ = this.onFocusIn_.bind(this); 44 this.addEventListener('mousedown', this.onMousedown_.bind(this)); 45 }, 46 47 /** 48 * Adds an overlay, attaching it to the contentHost so that it is visible. 49 */ 50 showOverlay: function(overlay) { 51 // Reparent this to the overlay content host. 52 overlay.oldParent_ = overlay.parentNode; 53 this.contentHost.appendChild(overlay); 54 this.contentHost.appendChild(this.tabCatcher); 55 56 // Show the overlay root. 57 this.ownerDocument.body.classList.add('disabled-by-overlay'); 58 this.visible = true; 59 60 // Bring overlay into focus. 61 overlay.tabIndex = 0; 62 var focusElement = 63 overlay.querySelector('button, input, list, select, a'); 64 if (!focusElement) { 65 focusElement = overlay; 66 } 67 focusElement.focus(); 68 69 // Listen to key and focus events to prevent focus from 70 // leaving the overlay. 71 this.ownerDocument.addEventListener('focusin', 72 this.onFocusInBoundToThis_, true); 73 overlay.addEventListener('keydown', this.onKeydownBoundToThis_); 74 }, 75 76 /** 77 * Clicking outside of the overlay will de-focus the overlay. The 78 * next tab will look at the entire document to determine the focus. 79 * For certain documents, this can cause focus to "leak" outside of 80 * the overlay. 81 */ 82 onMousedown_: function(e) { 83 if (e.target == this) { 84 e.preventDefault(); 85 } 86 }, 87 88 /** 89 * Prevents forward-tabbing out of the overlay 90 */ 91 onFocusIn_: function(e) { 92 if (e.target == this.tabCatcher) { 93 window.setTimeout(this.focusOverlay_.bind(this), 0); 94 } 95 }, 96 97 focusOverlay_: function() { 98 this.contentHost.firstChild.focus(); 99 }, 100 101 /** 102 * Prevent the user from shift-tabbing backwards out of the overlay. 103 */ 104 onKeydown_: function(e) { 105 if (e.keyCode == 9 && // tab 106 e.shiftKey && 107 e.target == this.contentHost.firstChild) { 108 e.preventDefault(); 109 } 110 }, 111 112 /** 113 * Hides an overlay, attaching it to its original parent if needed. 114 */ 115 hideOverlay: function(overlay) { 116 // hide the overlay root 117 this.visible = false; 118 this.ownerDocument.body.classList.remove('disabled-by-overlay'); 119 this.lastFocusOut_ = undefined; 120 121 // put the overlay back on its previous parent 122 overlay.parentNode.removeChild(this.tabCatcher); 123 if (overlay.oldParent_) { 124 overlay.oldParent_.appendChild(overlay); 125 delete overlay.oldParent_; 126 } else { 127 this.contentHost.removeChild(overlay); 128 } 129 130 // remove listeners 131 overlay.removeEventListener('keydown', this.onKeydownBoundToThis_); 132 this.ownerDocument.removeEventListener('focusin', 133 this.onFocusInBoundToThis_); 134 } 135 }; 136 137 base.defineProperty(OverlayRoot, 'visible', base.PropertyKind.BOOL_ATTR); 138 139 /** 140 * Creates a new overlay element. It will not be visible until shown. 141 * @param {Object=} opt_propertyBag Optional properties. 142 * @constructor 143 * @extends {HTMLDivElement} 144 */ 145 var Overlay = tracing.ui.define('div'); 146 147 Overlay.prototype = { 148 __proto__: HTMLDivElement.prototype, 149 150 /** 151 * Initializes the overlay element. 152 */ 153 decorate: function() { 154 // create the overlay root on this document if its not present 155 if (!this.ownerDocument.querySelector('.overlay-root')) { 156 var overlayRoot = this.ownerDocument.createElement('div'); 157 tracing.ui.decorate(overlayRoot, OverlayRoot); 158 this.ownerDocument.body.appendChild(overlayRoot); 159 } 160 161 this.classList.add('overlay'); 162 this.visible = false; 163 this.defaultClickShouldClose = true; 164 this.autoClose = false; 165 this.additionalCloseKeyCodes = []; 166 this.onKeyDown = this.onKeyDown.bind(this); 167 this.onKeyPress = this.onKeyPress.bind(this); 168 this.onDocumentClick = this.onDocumentClick.bind(this); 169 }, 170 171 onVisibleChanged_: function() { 172 var overlayRoot = this.ownerDocument.querySelector('.overlay-root'); 173 base.dispatchSimpleEvent(this, 'visibleChange'); 174 if (this.visible) { 175 overlayRoot.showOverlay(this); 176 document.addEventListener('keydown', this.onKeyDown, true); 177 document.addEventListener('keypress', this.onKeyPress, true); 178 document.addEventListener('click', this.onDocumentClick, true); 179 } else { 180 document.removeEventListener('keydown', this.onKeyDown, true); 181 document.removeEventListener('keypress', this.onKeyPress, true); 182 document.removeEventListener('click', this.onDocumentClick, true); 183 overlayRoot.hideOverlay(this); 184 } 185 }, 186 187 onKeyDown: function(e) { 188 if (!this.autoClose) 189 return; 190 191 if (e.keyCode == 27) { // escape 192 this.visible = false; 193 e.preventDefault(); 194 return; 195 } 196 }, 197 198 onKeyPress: function(e) { 199 if (!this.autoClose) 200 return; 201 202 for (var i = 0; i < this.additionalCloseKeyCodes.length; i++) { 203 if (e.keyCode == this.additionalCloseKeyCodes[i]) { 204 this.visible = false; 205 e.preventDefault(); 206 return; 207 } 208 } 209 }, 210 211 onDocumentClick: function(e) { 212 if (!this.defaultClickShouldClose) 213 return; 214 var target = e.target; 215 while (target !== null) { 216 if (target === this) 217 return; 218 target = target.parentNode; 219 } 220 this.visible = false; 221 e.preventDefault(); 222 return; 223 } 224 225 }; 226 227 /** 228 * Shows and hides the overlay. Note that while visible == true, the overlay 229 * element will be tempoarily reparented to another place in the DOM. 230 */ 231 base.defineProperty(Overlay, 'visible', base.PropertyKind.BOOL_ATTR, 232 Overlay.prototype.onVisibleChanged_); 233 base.defineProperty(Overlay, 'defaultClickShouldClose', 234 base.PropertyKind.BOOL_ATTR); 235 236 return { 237 Overlay: Overlay 238 }; 239 }); 240