blob: 648f9c8cc87ce11cd34ee62e02118551391f823e [file] [log] [blame]
Jamie Gennis01058352012-05-06 12:48:05 -07001// 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});