• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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   * @constructor
8   */
9  function BaseDialog(parentNode) {
10    this.parentNode_ = parentNode;
11    this.document_ = parentNode.ownerDocument;
12
13    // The DOM element from the dialog which should receive focus when the
14    // dialog is first displayed.
15    this.initialFocusElement_ = null;
16
17    // The DOM element from the parent which had focus before we were displayed,
18    // so we can restore it when we're hidden.
19    this.previousActiveElement_ = null;
20
21    this.initDom_();
22  }
23
24  /**
25   * Default text for Ok and Cancel buttons.
26   *
27   * Clients should override these with localized labels.
28   */
29  BaseDialog.OK_LABEL = '[LOCALIZE ME] Ok';
30  BaseDialog.CANCEL_LABEL = '[LOCALIZE ME] Cancel';
31
32  /**
33   * Number of miliseconds animation is expected to take, plus some margin for
34   * error.
35   */
36  BaseDialog.ANIMATE_STABLE_DURATION = 500;
37
38  BaseDialog.prototype.initDom_ = function() {
39    var doc = this.document_;
40    this.container_ = doc.createElement('div');
41    this.container_.className = 'cr-dialog-container';
42    this.container_.addEventListener('keydown',
43                                     this.onContainerKeyDown_.bind(this));
44    this.shield_ = doc.createElement('div');
45    this.shield_.className = 'cr-dialog-shield';
46    this.container_.appendChild(this.shield_);
47    this.container_.addEventListener('mousedown',
48                                     this.onContainerMouseDown_.bind(this));
49
50    this.frame_ = doc.createElement('div');
51    this.frame_.className = 'cr-dialog-frame';
52    // Elements that have negative tabIndex can be focused but are not traversed
53    // by Tab key.
54    this.frame_.tabIndex = -1;
55    this.container_.appendChild(this.frame_);
56
57    this.title_ = doc.createElement('div');
58    this.title_.className = 'cr-dialog-title';
59    this.frame_.appendChild(this.title_);
60
61    this.closeButton_ = doc.createElement('div');
62    this.closeButton_.className = 'cr-dialog-close';
63    this.closeButton_.addEventListener('click',
64                                        this.onCancelClick_.bind(this));
65    this.frame_.appendChild(this.closeButton_);
66
67    this.text_ = doc.createElement('div');
68    this.text_.className = 'cr-dialog-text';
69    this.frame_.appendChild(this.text_);
70
71    this.buttons = doc.createElement('div');
72    this.buttons.className = 'cr-dialog-buttons';
73    this.frame_.appendChild(this.buttons);
74
75    this.okButton_ = doc.createElement('button');
76    this.okButton_.className = 'cr-dialog-ok';
77    this.okButton_.textContent = BaseDialog.OK_LABEL;
78    this.okButton_.addEventListener('click', this.onOkClick_.bind(this));
79    this.buttons.appendChild(this.okButton_);
80
81    this.cancelButton_ = doc.createElement('button');
82    this.cancelButton_.className = 'cr-dialog-cancel';
83    this.cancelButton_.textContent = BaseDialog.CANCEL_LABEL;
84    this.cancelButton_.addEventListener('click',
85                                        this.onCancelClick_.bind(this));
86    this.buttons.appendChild(this.cancelButton_);
87
88    this.initialFocusElement_ = this.okButton_;
89  };
90
91  BaseDialog.prototype.onOk_ = null;
92  BaseDialog.prototype.onCancel_ = null;
93
94  BaseDialog.prototype.onContainerKeyDown_ = function(event) {
95    // Handle Escape.
96    if (event.keyCode == 27 && !this.cancelButton_.disabled) {
97      this.onCancelClick_(event);
98      event.stopPropagation();
99      // Prevent the event from being handled by the container of the dialog.
100      // e.g. Prevent the parent container from closing at the same time.
101      event.preventDefault();
102    }
103  };
104
105  BaseDialog.prototype.onContainerMouseDown_ = function(event) {
106    if (event.target == this.container_) {
107      var classList = this.frame_.classList;
108      // Start 'pulse' animation.
109      classList.remove('pulse');
110      setTimeout(classList.add.bind(classList, 'pulse'), 0);
111      event.preventDefault();
112    }
113  };
114
115  BaseDialog.prototype.onOkClick_ = function(event) {
116    this.hide();
117    if (this.onOk_)
118      this.onOk_();
119  };
120
121  BaseDialog.prototype.onCancelClick_ = function(event) {
122    this.hide();
123    if (this.onCancel_)
124      this.onCancel_();
125  };
126
127  BaseDialog.prototype.setOkLabel = function(label) {
128    this.okButton_.textContent = label;
129  };
130
131  BaseDialog.prototype.setCancelLabel = function(label) {
132    this.cancelButton_.textContent = label;
133  };
134
135  BaseDialog.prototype.setInitialFocusOnCancel = function() {
136    this.initialFocusElement_ = this.cancelButton_;
137  };
138
139  BaseDialog.prototype.show = function(message, onOk, onCancel, onShow) {
140    this.showWithTitle(null, message, onOk, onCancel, onShow);
141  };
142
143  BaseDialog.prototype.showHtml = function(title, message,
144      onOk, onCancel, onShow) {
145    this.text_.innerHTML = message;
146    this.show_(title, onOk, onCancel, onShow);
147  };
148
149  BaseDialog.prototype.findFocusableElements_ = function(doc) {
150    var elements = Array.prototype.filter.call(
151        doc.querySelectorAll('*'),
152        function(n) { return n.tabIndex >= 0; });
153
154    var iframes = doc.querySelectorAll('iframe');
155    for (var i = 0; i < iframes.length; i++) {
156      // Some iframes have an undefined contentDocument for security reasons,
157      // such as chrome://terms (which is used in the chromeos OOBE screens).
158      var iframe = iframes[i];
159      var contentDoc;
160      try {
161        contentDoc = iframe.contentDocument;
162      } catch(e) {} // ignore SecurityError
163      if (contentDoc)
164        elements = elements.concat(this.findFocusableElements_(contentDoc));
165    }
166    return elements;
167  };
168
169  BaseDialog.prototype.showWithTitle = function(title, message,
170      onOk, onCancel, onShow) {
171    this.text_.textContent = message;
172    this.show_(title, onOk, onCancel, onShow);
173  };
174
175  BaseDialog.prototype.show_ = function(title, onOk, onCancel, onShow) {
176    // Make all outside nodes unfocusable while the dialog is active.
177    this.deactivatedNodes_ = this.findFocusableElements_(this.document_);
178    this.tabIndexes_ = this.deactivatedNodes_.map(
179        function(n) { return n.getAttribute('tabindex'); });
180    this.deactivatedNodes_.forEach(
181        function(n) { n.tabIndex = -1; });
182
183    this.previousActiveElement_ = this.document_.activeElement;
184    this.parentNode_.appendChild(this.container_);
185
186    this.onOk_ = onOk;
187    this.onCancel_ = onCancel;
188
189    if (title) {
190      this.title_.textContent = title;
191      this.title_.hidden = false;
192    } else {
193      this.title_.textContent = '';
194      this.title_.hidden = true;
195    }
196
197    var self = this;
198    setTimeout(function() {
199      // Note that we control the opacity of the *container*, but the top/left
200      // of the *frame*.
201      self.container_.classList.add('shown');
202      self.initialFocusElement_.focus();
203      setTimeout(function() {
204        if (onShow)
205          onShow();
206      }, BaseDialog.ANIMATE_STABLE_DURATION);
207    }, 0);
208  };
209
210  /**
211   * @param {Function=} opt_onHide
212   */
213  BaseDialog.prototype.hide = function(opt_onHide) {
214    // Restore focusability.
215    for (var i = 0; i < this.deactivatedNodes_.length; i++) {
216      var node = this.deactivatedNodes_[i];
217      if (this.tabIndexes_[i] === null)
218        node.removeAttribute('tabindex');
219      else
220        node.setAttribute('tabindex', this.tabIndexes_[i]);
221    }
222    this.deactivatedNodes_ = null;
223    this.tabIndexes_ = null;
224
225    // Note that we control the opacity of the *container*, but the top/left
226    // of the *frame*.
227    this.container_.classList.remove('shown');
228
229    if (this.previousActiveElement_) {
230      this.previousActiveElement_.focus();
231    } else {
232      this.document_.body.focus();
233    }
234    this.frame_.classList.remove('pulse');
235
236    var self = this;
237    setTimeout(function() {
238      // Wait until the transition is done before removing the dialog.
239      self.parentNode_.removeChild(self.container_);
240      if (opt_onHide)
241        opt_onHide();
242    }, BaseDialog.ANIMATE_STABLE_DURATION);
243  };
244
245  /**
246   * AlertDialog contains just a message and an ok button.
247   * @constructor
248   * @extends {cr.ui.dialogs.BaseDialog}
249   */
250  function AlertDialog(parentNode) {
251    BaseDialog.apply(this, [parentNode]);
252    this.cancelButton_.style.display = 'none';
253  }
254
255  AlertDialog.prototype = {__proto__: BaseDialog.prototype};
256
257  AlertDialog.prototype.show = function(message, onOk, onShow) {
258    return BaseDialog.prototype.show.apply(this, [message, onOk, onOk, onShow]);
259  };
260
261  /**
262   * ConfirmDialog contains a message, an ok button, and a cancel button.
263   * @constructor
264   * @extends {cr.ui.dialogs.BaseDialog}
265   */
266  function ConfirmDialog(parentNode) {
267    BaseDialog.apply(this, [parentNode]);
268  }
269
270  ConfirmDialog.prototype = {__proto__: BaseDialog.prototype};
271
272  /**
273   * PromptDialog contains a message, a text input, an ok button, and a
274   * cancel button.
275   * @constructor
276   * @extends {cr.ui.dialogs.BaseDialog}
277   */
278  function PromptDialog(parentNode) {
279    BaseDialog.apply(this, [parentNode]);
280    this.input_ = this.document_.createElement('input');
281    this.input_.setAttribute('type', 'text');
282    this.input_.addEventListener('focus', this.onInputFocus.bind(this));
283    this.input_.addEventListener('keydown', this.onKeyDown_.bind(this));
284    this.initialFocusElement_ = this.input_;
285    this.frame_.insertBefore(this.input_, this.text_.nextSibling);
286  }
287
288  PromptDialog.prototype = {__proto__: BaseDialog.prototype};
289
290  PromptDialog.prototype.onInputFocus = function(event) {
291    this.input_.select();
292  };
293
294  PromptDialog.prototype.onKeyDown_ = function(event) {
295    if (event.keyCode == 13) {  // Enter
296      this.onOkClick_(event);
297      event.preventDefault();
298    }
299  };
300
301  PromptDialog.prototype.show = function(message, defaultValue, onOk, onCancel,
302                                        onShow) {
303    this.input_.value = defaultValue || '';
304    return BaseDialog.prototype.show.apply(this, [message, onOk, onCancel,
305                                                  onShow]);
306  };
307
308  PromptDialog.prototype.getValue = function() {
309    return this.input_.value;
310  };
311
312  PromptDialog.prototype.onOkClick_ = function(event) {
313    this.hide();
314    if (this.onOk_)
315      this.onOk_(this.getValue());
316  };
317
318  return {
319    BaseDialog: BaseDialog,
320    AlertDialog: AlertDialog,
321    ConfirmDialog: ConfirmDialog,
322    PromptDialog: PromptDialog
323  };
324});
325