dialogs.js revision 4e180b6a0b4720a9b8e9e959a882386f690f08ff
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