Home | History | Annotate | Download | only in login
      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 /**
      6  * @fileoverview Bubble implementation.
      7  */
      8 
      9 // TODO(xiyuan): Move this into shared.
     10 cr.define('cr.ui', function() {
     11   /**
     12    * Creates a bubble div.
     13    * @constructor
     14    * @extends {HTMLDivElement}
     15    */
     16   var Bubble = cr.ui.define('div');
     17 
     18   /**
     19    * Bubble attachment side.
     20    * @enum {string}
     21    */
     22   Bubble.Attachment = {
     23     RIGHT: 'bubble-right',
     24     LEFT: 'bubble-left',
     25     TOP: 'bubble-top',
     26     BOTTOM: 'bubble-bottom'
     27   };
     28 
     29   Bubble.prototype = {
     30     __proto__: HTMLDivElement.prototype,
     31 
     32     // Anchor element for this bubble.
     33     anchor_: undefined,
     34 
     35     // If defined, sets focus to this element once bubble is closed. Focus is
     36     // set to this element only if there's no any other focused element.
     37     elementToFocusOnHide_: undefined,
     38 
     39     // Whether to hide bubble when key is pressed.
     40     hideOnKeyPress_: true,
     41 
     42     /** @override */
     43     decorate: function() {
     44       this.docKeyDownHandler_ = this.handleDocKeyDown_.bind(this);
     45       this.selfClickHandler_ = this.handleSelfClick_.bind(this);
     46       this.ownerDocument.addEventListener('click',
     47                                           this.handleDocClick_.bind(this));
     48       this.ownerDocument.addEventListener('keydown',
     49                                           this.docKeyDownHandler_);
     50       window.addEventListener('blur', this.handleWindowBlur_.bind(this));
     51       this.addEventListener('webkitTransitionEnd',
     52                             this.handleTransitionEnd_.bind(this));
     53       // Guard timer for 200ms + epsilon.
     54       ensureTransitionEndEvent(this, 250);
     55     },
     56 
     57     /**
     58      * Element that should be focused on hide.
     59      * @type {HTMLElement}
     60      */
     61     set elementToFocusOnHide(value) {
     62       this.elementToFocusOnHide_ = value;
     63     },
     64 
     65     /**
     66      * Whether to hide bubble when key is pressed.
     67      * @type {boolean}
     68      */
     69     set hideOnKeyPress(value) {
     70       this.hideOnKeyPress_ = value;
     71     },
     72 
     73     /**
     74      * Whether to hide bubble when clicked inside bubble element.
     75      * Default is true.
     76      * @type {boolean}
     77      */
     78     set hideOnSelfClick(value) {
     79       if (value)
     80         this.removeEventListener('click', this.selfClickHandler_);
     81       else
     82         this.addEventListener('click', this.selfClickHandler_);
     83     },
     84 
     85     /**
     86      * Handler for click event which prevents bubble auto hide.
     87      * @private
     88      */
     89     handleSelfClick_: function(e) {
     90       // Allow clicking on [x] button.
     91       if (e.target && e.target.classList.contains('close-button'))
     92         return;
     93 
     94       e.stopPropagation();
     95     },
     96 
     97     /**
     98      * Sets the attachment of the bubble.
     99      * @param {!Attachment} attachment Bubble attachment.
    100      */
    101     setAttachment_: function(attachment) {
    102       for (var k in Bubble.Attachment) {
    103         var v = Bubble.Attachment[k];
    104         this.classList.toggle(v, v == attachment);
    105       }
    106     },
    107 
    108     /**
    109      * Shows the bubble for given anchor element.
    110      * @param {!Object} pos Bubble position (left, top, right, bottom in px).
    111      * @param {!Attachment} attachment Bubble attachment (on which side of the
    112      *     specified position it should be displayed).
    113      * @param {HTMLElement} opt_content Content to show in bubble.
    114      *     If not specified, bubble element content is shown.
    115      * @private
    116      */
    117     showContentAt_: function(pos, attachment, opt_content) {
    118       this.style.top = this.style.left = this.style.right = this.style.bottom =
    119           'auto';
    120       for (var k in pos) {
    121         if (typeof pos[k] == 'number')
    122           this.style[k] = pos[k] + 'px';
    123       }
    124       if (opt_content !== undefined) {
    125         this.innerHTML = '';
    126         this.appendChild(opt_content);
    127       }
    128       this.setAttachment_(attachment);
    129       this.hidden = false;
    130       this.classList.remove('faded');
    131     },
    132 
    133     /**
    134      * Shows the bubble for given anchor element. Bubble content is not cleared.
    135      * @param {!HTMLElement} el Anchor element of the bubble.
    136      * @param {!Attachment} attachment Bubble attachment (on which side of the
    137      *     element it should be displayed).
    138      * @param {number=} opt_offset Offset of the bubble.
    139      * @param {number=} opt_padding Optional padding of the bubble.
    140      */
    141     showForElement: function(el, attachment, opt_offset, opt_padding) {
    142       this.showContentForElement(
    143           el, attachment, undefined, opt_offset, opt_padding);
    144     },
    145 
    146     /**
    147      * Shows the bubble for given anchor element.
    148      * @param {!HTMLElement} el Anchor element of the bubble.
    149      * @param {!Attachment} attachment Bubble attachment (on which side of the
    150      *     element it should be displayed).
    151      * @param {HTMLElement} opt_content Content to show in bubble.
    152      *     If not specified, bubble element content is shown.
    153      * @param {number=} opt_offset Offset of the bubble attachment point from
    154      *     left (for vertical attachment) or top (for horizontal attachment)
    155      *     side of the element. If not specified, the bubble is positioned to
    156      *     be aligned with the left/top side of the element but not farther than
    157      *     half of its width/height.
    158      * @param {number=} opt_padding Optional padding of the bubble.
    159      */
    160     showContentForElement: function(el, attachment, opt_content,
    161                                     opt_offset, opt_padding) {
    162       /** @const */ var ARROW_OFFSET = 25;
    163       /** @const */ var DEFAULT_PADDING = 18;
    164 
    165       if (opt_padding == undefined)
    166         opt_padding = DEFAULT_PADDING;
    167 
    168       var origin = cr.ui.login.DisplayManager.getPosition(el);
    169       var offset = opt_offset == undefined ?
    170           [Math.min(ARROW_OFFSET, el.offsetWidth / 2),
    171            Math.min(ARROW_OFFSET, el.offsetHeight / 2)] :
    172           [opt_offset, opt_offset];
    173 
    174       var pos = {};
    175       if (isRTL()) {
    176         switch (attachment) {
    177           case Bubble.Attachment.TOP:
    178             pos.right = origin.right + offset[0] - ARROW_OFFSET;
    179             pos.bottom = origin.bottom + el.offsetHeight + opt_padding;
    180             break;
    181           case Bubble.Attachment.RIGHT:
    182             pos.top = origin.top + offset[1] - ARROW_OFFSET;
    183             pos.right = origin.right + el.offsetWidth + opt_padding;
    184             break;
    185           case Bubble.Attachment.BOTTOM:
    186             pos.right = origin.right + offset[0] - ARROW_OFFSET;
    187             pos.top = origin.top + el.offsetHeight + opt_padding;
    188             break;
    189           case Bubble.Attachment.LEFT:
    190             pos.top = origin.top + offset[1] - ARROW_OFFSET;
    191             pos.left = origin.left + el.offsetWidth + opt_padding;
    192             break;
    193         }
    194       } else {
    195         switch (attachment) {
    196           case Bubble.Attachment.TOP:
    197             pos.left = origin.left + offset[0] - ARROW_OFFSET;
    198             pos.bottom = origin.bottom + el.offsetHeight + opt_padding;
    199             break;
    200           case Bubble.Attachment.RIGHT:
    201             pos.top = origin.top + offset[1] - ARROW_OFFSET;
    202             pos.left = origin.left + el.offsetWidth + opt_padding;
    203             break;
    204           case Bubble.Attachment.BOTTOM:
    205             pos.left = origin.left + offset[0] - ARROW_OFFSET;
    206             pos.top = origin.top + el.offsetHeight + opt_padding;
    207             break;
    208           case Bubble.Attachment.LEFT:
    209             pos.top = origin.top + offset[1] - ARROW_OFFSET;
    210             pos.right = origin.right + el.offsetWidth + opt_padding;
    211             break;
    212         }
    213       }
    214 
    215       this.anchor_ = el;
    216       this.showContentAt_(pos, attachment, opt_content);
    217     },
    218 
    219     /**
    220      * Shows the bubble for given anchor element.
    221      * @param {!HTMLElement} el Anchor element of the bubble.
    222      * @param {string} text Text content to show in bubble.
    223      * @param {!Attachment} attachment Bubble attachment (on which side of the
    224      *     element it should be displayed).
    225      * @param {number=} opt_offset Offset of the bubble attachment point from
    226      *     left (for vertical attachment) or top (for horizontal attachment)
    227      *     side of the element. If not specified, the bubble is positioned to
    228      *     be aligned with the left/top side of the element but not farther than
    229      *     half of its weight/height.
    230      * @param {number=} opt_padding Optional padding of the bubble.
    231      */
    232     showTextForElement: function(el, text, attachment,
    233                                  opt_offset, opt_padding) {
    234       var span = this.ownerDocument.createElement('span');
    235       span.textContent = text;
    236       this.showContentForElement(el, attachment, span, opt_offset, opt_padding);
    237     },
    238 
    239     /**
    240      * Hides the bubble.
    241      */
    242     hide: function() {
    243       if (!this.classList.contains('faded'))
    244         this.classList.add('faded');
    245     },
    246 
    247     /**
    248      * Hides the bubble anchored to the given element (if any).
    249      * @param {!Object} el Anchor element.
    250      */
    251     hideForElement: function(el) {
    252       if (!this.hidden && this.anchor_ == el)
    253         this.hide();
    254     },
    255 
    256     /**
    257      * Handler for faded transition end.
    258      * @private
    259      */
    260     handleTransitionEnd_: function(e) {
    261       if (this.classList.contains('faded')) {
    262         this.hidden = true;
    263         if (this.elementToFocusOnHide_ &&
    264             document.activeElement == document.body) {
    265           // Restore focus to default element only if there's no other
    266           // element that is focused.
    267           this.elementToFocusOnHide_.focus();
    268         }
    269       }
    270     },
    271 
    272     /**
    273      * Handler of document click event.
    274      * @private
    275      */
    276     handleDocClick_: function(e) {
    277       // Ignore clicks on anchor element.
    278       if (e.target == this.anchor_)
    279         return;
    280 
    281       if (!this.hidden)
    282         this.hide();
    283     },
    284 
    285     /**
    286      * Handle of document keydown event.
    287      * @private
    288      */
    289     handleDocKeyDown_: function(e) {
    290       if (this.hideOnKeyPress_ && !this.hidden) {
    291         this.hide();
    292         return;
    293       }
    294 
    295       if (e.keyCode == 27 && !this.hidden) {
    296         if (this.elementToFocusOnHide_)
    297           this.elementToFocusOnHide_.focus();
    298         this.hide();
    299       }
    300     },
    301 
    302     /**
    303      * Handler of window blur event.
    304      * @private
    305      */
    306     handleWindowBlur_: function(e) {
    307       if (!this.hidden)
    308         this.hide();
    309     }
    310   };
    311 
    312   return {
    313     Bubble: Bubble
    314   };
    315 });
    316