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 5cr.define('cr.ui.dialogs', function() { 6 /** 7 * @constructor 8 */ 9 function BaseDialog(parentNode) { 10 this.parentNode_ = parentNode; 11 this.document_ = parentNode.ownerDocument; 12 13 // The DOM element from the dialog which should receive focus when the 14 // dialog is first displayed. 15 this.initialFocusElement_ = null; 16 17 // The DOM element from the parent which had focus before we were displayed, 18 // so we can restore it when we're hidden. 19 this.previousActiveElement_ = null; 20 21 this.initDom_(); 22 } 23 24 /** 25 * Default text for Ok and Cancel buttons. 26 * 27 * Clients should override these with localized labels. 28 */ 29 BaseDialog.OK_LABEL = '[LOCALIZE ME] Ok'; 30 BaseDialog.CANCEL_LABEL = '[LOCALIZE ME] Cancel'; 31 32 /** 33 * Number of miliseconds animation is expected to take, plus some margin for 34 * error. 35 */ 36 BaseDialog.ANIMATE_STABLE_DURATION = 500; 37 38 BaseDialog.prototype.initDom_ = function() { 39 var doc = this.document_; 40 this.container_ = doc.createElement('div'); 41 this.container_.className = 'cr-dialog-container'; 42 this.container_.addEventListener('keydown', 43 this.onContainerKeyDown_.bind(this)); 44 this.shield_ = doc.createElement('div'); 45 this.shield_.className = 'cr-dialog-shield'; 46 this.container_.appendChild(this.shield_); 47 this.container_.addEventListener('mousedown', 48 this.onContainerMouseDown_.bind(this)); 49 50 this.frame_ = doc.createElement('div'); 51 this.frame_.className = 'cr-dialog-frame'; 52 // Elements that have negative tabIndex can be focused but are not traversed 53 // by Tab key. 54 this.frame_.tabIndex = -1; 55 this.container_.appendChild(this.frame_); 56 57 this.title_ = doc.createElement('div'); 58 this.title_.className = 'cr-dialog-title'; 59 this.frame_.appendChild(this.title_); 60 61 this.closeButton_ = doc.createElement('div'); 62 this.closeButton_.className = 'cr-dialog-close'; 63 this.closeButton_.addEventListener('click', 64 this.onCancelClick_.bind(this)); 65 this.frame_.appendChild(this.closeButton_); 66 67 this.text_ = doc.createElement('div'); 68 this.text_.className = 'cr-dialog-text'; 69 this.frame_.appendChild(this.text_); 70 71 this.buttons = doc.createElement('div'); 72 this.buttons.className = 'cr-dialog-buttons'; 73 this.frame_.appendChild(this.buttons); 74 75 this.okButton_ = doc.createElement('button'); 76 this.okButton_.className = 'cr-dialog-ok'; 77 this.okButton_.textContent = BaseDialog.OK_LABEL; 78 this.okButton_.addEventListener('click', this.onOkClick_.bind(this)); 79 this.buttons.appendChild(this.okButton_); 80 81 this.cancelButton_ = doc.createElement('button'); 82 this.cancelButton_.className = 'cr-dialog-cancel'; 83 this.cancelButton_.textContent = BaseDialog.CANCEL_LABEL; 84 this.cancelButton_.addEventListener('click', 85 this.onCancelClick_.bind(this)); 86 this.buttons.appendChild(this.cancelButton_); 87 88 this.initialFocusElement_ = this.okButton_; 89 }; 90 91 BaseDialog.prototype.onOk_ = null; 92 BaseDialog.prototype.onCancel_ = null; 93 94 BaseDialog.prototype.onContainerKeyDown_ = function(event) { 95 // Handle Escape. 96 if (event.keyCode == 27 && !this.cancelButton_.disabled) { 97 this.onCancelClick_(event); 98 event.stopPropagation(); 99 // Prevent the event from being handled by the container of the dialog. 100 // e.g. Prevent the parent container from closing at the same time. 101 event.preventDefault(); 102 } 103 }; 104 105 BaseDialog.prototype.onContainerMouseDown_ = function(event) { 106 if (event.target == this.container_) { 107 var classList = this.frame_.classList; 108 // Start 'pulse' animation. 109 classList.remove('pulse'); 110 setTimeout(classList.add.bind(classList, 'pulse'), 0); 111 event.preventDefault(); 112 } 113 }; 114 115 BaseDialog.prototype.onOkClick_ = function(event) { 116 this.hide(); 117 if (this.onOk_) 118 this.onOk_(); 119 }; 120 121 BaseDialog.prototype.onCancelClick_ = function(event) { 122 this.hide(); 123 if (this.onCancel_) 124 this.onCancel_(); 125 }; 126 127 BaseDialog.prototype.setOkLabel = function(label) { 128 this.okButton_.textContent = label; 129 }; 130 131 BaseDialog.prototype.setCancelLabel = function(label) { 132 this.cancelButton_.textContent = label; 133 }; 134 135 BaseDialog.prototype.setInitialFocusOnCancel = function() { 136 this.initialFocusElement_ = this.cancelButton_; 137 }; 138 139 BaseDialog.prototype.show = function(message, onOk, onCancel, onShow) { 140 this.showWithTitle(null, message, onOk, onCancel, onShow); 141 }; 142 143 BaseDialog.prototype.showHtml = function(title, message, 144 onOk, onCancel, onShow) { 145 this.text_.innerHTML = message; 146 this.show_(title, onOk, onCancel, onShow); 147 }; 148 149 BaseDialog.prototype.findFocusableElements_ = function(doc) { 150 var elements = Array.prototype.filter.call( 151 doc.querySelectorAll('*'), 152 function(n) { return n.tabIndex >= 0; }); 153 154 var iframes = doc.querySelectorAll('iframe'); 155 for (var i = 0; i < iframes.length; i++) { 156 // Some iframes have an undefined contentDocument for security reasons, 157 // such as chrome://terms (which is used in the chromeos OOBE screens). 158 var iframe = iframes[i]; 159 var contentDoc; 160 try { 161 contentDoc = iframe.contentDocument; 162 } catch(e) {} // ignore SecurityError 163 if (contentDoc) 164 elements = elements.concat(this.findFocusableElements_(contentDoc)); 165 } 166 return elements; 167 }; 168 169 BaseDialog.prototype.showWithTitle = function(title, message, 170 onOk, onCancel, onShow) { 171 this.text_.textContent = message; 172 this.show_(title, onOk, onCancel, onShow); 173 }; 174 175 BaseDialog.prototype.show_ = function(title, onOk, onCancel, onShow) { 176 // Make all outside nodes unfocusable while the dialog is active. 177 this.deactivatedNodes_ = this.findFocusableElements_(this.document_); 178 this.tabIndexes_ = this.deactivatedNodes_.map( 179 function(n) { return n.getAttribute('tabindex'); }); 180 this.deactivatedNodes_.forEach( 181 function(n) { n.tabIndex = -1; }); 182 183 this.previousActiveElement_ = this.document_.activeElement; 184 this.parentNode_.appendChild(this.container_); 185 186 this.onOk_ = onOk; 187 this.onCancel_ = onCancel; 188 189 if (title) { 190 this.title_.textContent = title; 191 this.title_.hidden = false; 192 } else { 193 this.title_.textContent = ''; 194 this.title_.hidden = true; 195 } 196 197 var self = this; 198 setTimeout(function() { 199 // Note that we control the opacity of the *container*, but the top/left 200 // of the *frame*. 201 self.container_.classList.add('shown'); 202 self.initialFocusElement_.focus(); 203 setTimeout(function() { 204 if (onShow) 205 onShow(); 206 }, BaseDialog.ANIMATE_STABLE_DURATION); 207 }, 0); 208 }; 209 210 /** 211 * @param {Function=} opt_onHide 212 */ 213 BaseDialog.prototype.hide = function(opt_onHide) { 214 // Restore focusability. 215 for (var i = 0; i < this.deactivatedNodes_.length; i++) { 216 var node = this.deactivatedNodes_[i]; 217 if (this.tabIndexes_[i] === null) 218 node.removeAttribute('tabindex'); 219 else 220 node.setAttribute('tabindex', this.tabIndexes_[i]); 221 } 222 this.deactivatedNodes_ = null; 223 this.tabIndexes_ = null; 224 225 // Note that we control the opacity of the *container*, but the top/left 226 // of the *frame*. 227 this.container_.classList.remove('shown'); 228 229 if (this.previousActiveElement_) { 230 this.previousActiveElement_.focus(); 231 } else { 232 this.document_.body.focus(); 233 } 234 this.frame_.classList.remove('pulse'); 235 236 var self = this; 237 setTimeout(function() { 238 // Wait until the transition is done before removing the dialog. 239 self.parentNode_.removeChild(self.container_); 240 if (opt_onHide) 241 opt_onHide(); 242 }, BaseDialog.ANIMATE_STABLE_DURATION); 243 }; 244 245 /** 246 * AlertDialog contains just a message and an ok button. 247 * @constructor 248 * @extends {cr.ui.dialogs.BaseDialog} 249 */ 250 function AlertDialog(parentNode) { 251 BaseDialog.apply(this, [parentNode]); 252 this.cancelButton_.style.display = 'none'; 253 } 254 255 AlertDialog.prototype = {__proto__: BaseDialog.prototype}; 256 257 AlertDialog.prototype.show = function(message, onOk, onShow) { 258 return BaseDialog.prototype.show.apply(this, [message, onOk, onOk, onShow]); 259 }; 260 261 /** 262 * ConfirmDialog contains a message, an ok button, and a cancel button. 263 * @constructor 264 * @extends {cr.ui.dialogs.BaseDialog} 265 */ 266 function ConfirmDialog(parentNode) { 267 BaseDialog.apply(this, [parentNode]); 268 } 269 270 ConfirmDialog.prototype = {__proto__: BaseDialog.prototype}; 271 272 /** 273 * PromptDialog contains a message, a text input, an ok button, and a 274 * cancel button. 275 * @constructor 276 * @extends {cr.ui.dialogs.BaseDialog} 277 */ 278 function PromptDialog(parentNode) { 279 BaseDialog.apply(this, [parentNode]); 280 this.input_ = this.document_.createElement('input'); 281 this.input_.setAttribute('type', 'text'); 282 this.input_.addEventListener('focus', this.onInputFocus.bind(this)); 283 this.input_.addEventListener('keydown', this.onKeyDown_.bind(this)); 284 this.initialFocusElement_ = this.input_; 285 this.frame_.insertBefore(this.input_, this.text_.nextSibling); 286 } 287 288 PromptDialog.prototype = {__proto__: BaseDialog.prototype}; 289 290 PromptDialog.prototype.onInputFocus = function(event) { 291 this.input_.select(); 292 }; 293 294 PromptDialog.prototype.onKeyDown_ = function(event) { 295 if (event.keyCode == 13) { // Enter 296 this.onOkClick_(event); 297 event.preventDefault(); 298 } 299 }; 300 301 PromptDialog.prototype.show = function(message, defaultValue, onOk, onCancel, 302 onShow) { 303 this.input_.value = defaultValue || ''; 304 return BaseDialog.prototype.show.apply(this, [message, onOk, onCancel, 305 onShow]); 306 }; 307 308 PromptDialog.prototype.getValue = function() { 309 return this.input_.value; 310 }; 311 312 PromptDialog.prototype.onOkClick_ = function(event) { 313 this.hide(); 314 if (this.onOk_) 315 this.onOk_(this.getValue()); 316 }; 317 318 return { 319 BaseDialog: BaseDialog, 320 AlertDialog: AlertDialog, 321 ConfirmDialog: ConfirmDialog, 322 PromptDialog: PromptDialog 323 }; 324}); 325