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