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   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