Home | History | Annotate | Download | only in ui
      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    * @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