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