Jamie Gennis | 0105835 | 2012-05-06 12:48:05 -0700 | [diff] [blame^] | 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 | 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 | }); |