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 'use strict'; 6 7 /** 8 * @fileoverview Implements an element that is hidden by default, but 9 * when shown, dims and (attempts to) disable the main document. 10 * 11 * You can turn any div into an overlay. Note that while an 12 * overlay element is shown, its parent is changed. Hiding the overlay 13 * restores its original parentage. 14 * 15 */ 16 base.requireStylesheet('ui.overlay'); 17 18 base.require('base.properties'); 19 base.require('base.events'); 20 base.require('ui'); 21 22 base.exportTo('ui', function() { 23 /** 24 * Manages a full-window div that darkens the window, disables 25 * input, and hosts the currently-visible overlays. You shouldn't 26 * have to instantiate this directly --- it gets set automatically. 27 * @param {Object=} opt_propertyBag Optional properties. 28 * @constructor 29 * @extends {HTMLDivElement} 30 */ 31 var OverlayRoot = ui.define('div'); 32 OverlayRoot.prototype = { 33 __proto__: HTMLDivElement.prototype, 34 decorate: function() { 35 this.classList.add('overlay-root'); 36 37 38 this.createToolBar_(); 39 40 this.contentHost = this.ownerDocument.createElement('div'); 41 this.contentHost.classList.add('content-host'); 42 43 this.tabCatcher = this.ownerDocument.createElement('span'); 44 this.tabCatcher.tabIndex = 0; 45 46 this.appendChild(this.contentHost); 47 48 this.onKeydown_ = this.onKeydown_.bind(this); 49 this.onFocusIn_ = this.onFocusIn_.bind(this); 50 this.addEventListener('mousedown', this.onMousedown_.bind(this)); 51 }, 52 53 toggleToolbar: function(show) { 54 if (show) { 55 if (this.contentHost.firstChild) 56 this.contentHost.insertBefore(this.contentHost.firstChild, 57 this.toolbar_); 58 else 59 this.contentHost.appendChild(this.toolbar_); 60 } else { 61 if (this.toolbar_.parentElement) 62 this.contentHost.removeChild(this.toolbar_); 63 } 64 }, 65 66 createToolBar_: function() { 67 this.toolbar_ = this.ownerDocument.createElement('div'); 68 this.toolbar_.className = 'tool-bar'; 69 this.exitButton_ = this.ownerDocument.createElement('span'); 70 this.exitButton_.className = 'exit-button'; 71 this.exitButton_.textContent = 'x'; 72 this.exitButton_.title = 'Close Overlay (esc)'; 73 this.toolbar_.appendChild(this.exitButton_); 74 }, 75 76 /** 77 * Adds an overlay, attaching it to the contentHost so that it is visible. 78 */ 79 showOverlay: function(overlay) { 80 // Reparent this to the overlay content host. 81 overlay.oldParent_ = overlay.parentNode; 82 this.contentHost.appendChild(overlay); 83 this.contentHost.appendChild(this.tabCatcher); 84 85 // Show the overlay root. 86 this.ownerDocument.body.classList.add('disabled-by-overlay'); 87 88 // Bring overlay into focus. 89 overlay.tabIndex = 0; 90 var focusElement = 91 overlay.querySelector('button, input, list, select, a'); 92 if (!focusElement) { 93 focusElement = overlay; 94 } 95 focusElement.focus(); 96 97 // Listen to key and focus events to prevent focus from 98 // leaving the overlay. 99 this.ownerDocument.addEventListener('focusin', this.onFocusIn_, true); 100 overlay.addEventListener('keydown', this.onKeydown_); 101 }, 102 103 /** 104 * Clicking outside of the overlay will de-focus the overlay. The 105 * next tab will look at the entire document to determine the focus. 106 * For certain documents, this can cause focus to "leak" outside of 107 * the overlay. 108 */ 109 onMousedown_: function(e) { 110 if (e.target == this) { 111 e.preventDefault(); 112 } 113 }, 114 115 /** 116 * Prevents forward-tabbing out of the overlay 117 */ 118 onFocusIn_: function(e) { 119 if (e.target == this.tabCatcher) { 120 window.setTimeout(this.focusOverlay_.bind(this), 0); 121 } 122 }, 123 124 focusOverlay_: function() { 125 this.contentHost.firstChild.focus(); 126 }, 127 128 /** 129 * Prevent the user from shift-tabbing backwards out of the overlay. 130 */ 131 onKeydown_: function(e) { 132 if (e.keyCode == 9 && // tab 133 e.shiftKey && 134 e.target == this.contentHost.firstChild) { 135 e.preventDefault(); 136 } 137 }, 138 139 /** 140 * Hides an overlay, attaching it to its original parent if needed. 141 */ 142 hideOverlay: function(overlay) { 143 // hide the overlay root 144 this.visible = false; 145 this.ownerDocument.body.classList.remove('disabled-by-overlay'); 146 this.lastFocusOut_ = undefined; 147 148 // put the overlay back on its previous parent 149 overlay.parentNode.removeChild(this.tabCatcher); 150 if (overlay.oldParent_) { 151 overlay.oldParent_.appendChild(overlay); 152 delete overlay.oldParent_; 153 } else { 154 this.contentHost.removeChild(overlay); 155 } 156 157 // remove listeners 158 overlay.removeEventListener('keydown', this.onKeydown_); 159 this.ownerDocument.removeEventListener('focusin', this.onFocusIn_); 160 } 161 }; 162 163 /** 164 * Creates a new overlay element. It will not be visible until shown. 165 * @param {Object=} opt_propertyBag Optional properties. 166 * @constructor 167 * @extends {HTMLDivElement} 168 */ 169 var Overlay = ui.define('div'); 170 171 Overlay.prototype = { 172 __proto__: HTMLDivElement.prototype, 173 174 /** 175 * Initializes the overlay element. 176 */ 177 decorate: function() { 178 // create the overlay root on this document if its not present 179 if (!this.ownerDocument.querySelector('.overlay-root')) { 180 var overlayRoot = this.ownerDocument.createElement('div'); 181 ui.decorate(overlayRoot, OverlayRoot); 182 this.ownerDocument.body.appendChild(overlayRoot); 183 } 184 185 this.classList.add('overlay'); 186 this.visible_ = false; 187 this.obeyCloseEvents = false; 188 this.additionalCloseKeyCodes = []; 189 this.onKeyDown = this.onKeyDown.bind(this); 190 this.onKeyPress = this.onKeyPress.bind(this); 191 this.onDocumentClick = this.onDocumentClick.bind(this); 192 this.addEventListener('visibleChange', 193 Overlay.prototype.onVisibleChange_.bind(this), true); 194 this.obeyCloseEvents = true; 195 }, 196 197 get visible() { 198 return this.visible_; 199 }, 200 201 set visible(newValue) { 202 base.setPropertyAndDispatchChange(this, 'visible', newValue); 203 }, 204 205 get obeyCloseEvents() { 206 return this.obeyCloseEvents_; 207 }, 208 209 set obeyCloseEvents(newValue) { 210 base.setPropertyAndDispatchChange(this, 'obeyCloseEvents', newValue); 211 var overlayRoot = this.ownerDocument.querySelector('.overlay-root'); 212 // Currently the toolbar only has the close button. 213 overlayRoot.toggleToolbar(newValue); 214 }, 215 216 get toolbar() { 217 return this.ownerDocument.querySelector('.overlay-root .tool-bar'); 218 }, 219 220 onVisibleChange_: function() { 221 var overlayRoot = this.ownerDocument.querySelector('.overlay-root'); 222 if (this.visible) { 223 overlayRoot.setAttribute('visible', 'visible'); 224 overlayRoot.showOverlay(this); 225 document.addEventListener('keydown', this.onKeyDown, true); 226 document.addEventListener('keypress', this.onKeyPress, true); 227 document.addEventListener('click', this.onDocumentClick, true); 228 } else { 229 overlayRoot.removeAttribute('visible'); 230 document.removeEventListener('keydown', this.onKeyDown, true); 231 document.removeEventListener('keypress', this.onKeyPress, true); 232 document.removeEventListener('click', this.onDocumentClick, true); 233 overlayRoot.hideOverlay(this); 234 } 235 }, 236 237 onKeyDown: function(e) { 238 if (!this.obeyCloseEvents) 239 return; 240 241 if (e.keyCode == 27) { // escape 242 this.visible = false; 243 e.preventDefault(); 244 return; 245 } 246 }, 247 248 onKeyPress: function(e) { 249 if (!this.obeyCloseEvents) 250 return; 251 252 for (var i = 0; i < this.additionalCloseKeyCodes.length; i++) { 253 if (e.keyCode == this.additionalCloseKeyCodes[i]) { 254 this.visible = false; 255 e.preventDefault(); 256 return; 257 } 258 } 259 }, 260 261 onDocumentClick: function(e) { 262 if (!this.obeyCloseEvents) 263 return; 264 var target = e.target; 265 while (target !== null) { 266 if (target === this) 267 return; 268 target = target.parentNode; 269 } 270 this.visible = false; 271 e.preventDefault(); 272 return; 273 } 274 275 }; 276 277 return { 278 Overlay: Overlay 279 }; 280 }); 281