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