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