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