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 // require: event_tracker.js
      6 
      7 cr.define('cr.ui', function() {
      8 
      9   /**
     10    * The arrow location specifies how the arrow and bubble are positioned in
     11    * relation to the anchor node.
     12    * @enum
     13    */
     14   var ArrowLocation = {
     15     // The arrow is positioned at the top and the start of the bubble. In left
     16     // to right mode this is the top left. The entire bubble is positioned below
     17     // the anchor node.
     18     TOP_START: 'top-start',
     19     // The arrow is positioned at the top and the end of the bubble. In left to
     20     // right mode this is the top right. The entire bubble is positioned below
     21     // the anchor node.
     22     TOP_END: 'top-end',
     23     // The arrow is positioned at the bottom and the start of the bubble. In
     24     // left to right mode this is the bottom left. The entire bubble is
     25     // positioned above the anchor node.
     26     BOTTOM_START: 'bottom-start',
     27     // The arrow is positioned at the bottom and the end of the bubble. In
     28     // left to right mode this is the bottom right. The entire bubble is
     29     // positioned above the anchor node.
     30     BOTTOM_END: 'bottom-end'
     31   };
     32 
     33   /**
     34    * The bubble alignment specifies the horizontal position of the bubble in
     35    * relation to the anchor node.
     36    * @enum
     37    */
     38   var BubbleAlignment = {
     39     // The bubble is positioned so that the tip of the arrow points to the
     40     // middle of the anchor node.
     41     ARROW_TO_MID_ANCHOR: 'arrow-to-mid-anchor',
     42     // The bubble is positioned so that the edge nearest to the arrow is lined
     43     // up with the edge of the anchor node.
     44     BUBBLE_EDGE_TO_ANCHOR_EDGE: 'bubble-edge-anchor-edge'
     45   };
     46 
     47   /**
     48    * The horizontal distance between the tip of the arrow and the start or the
     49    * end of the bubble (as specified by the arrow location).
     50    * @const
     51    */
     52   var ARROW_OFFSET_X = 30;
     53 
     54   /**
     55    * The vertical distance between the tip of the arrow and the bottom or top of
     56    * the bubble (as specified by the arrow location). Note, if you change this
     57    * then you should also change the "top" and "bottom" values for .bubble-arrow
     58    * in bubble.css.
     59    * @const
     60    */
     61   var ARROW_OFFSET_Y = 8;
     62 
     63   /**
     64    * Bubble is a free-floating informational bubble with a triangular arrow
     65    * that points at a place of interest on the page.
     66    */
     67   var Bubble = cr.ui.define('div');
     68 
     69   Bubble.prototype = {
     70     __proto__: HTMLDivElement.prototype,
     71 
     72     decorate: function() {
     73       this.className = 'bubble';
     74       this.innerHTML =
     75           '<div class="bubble-contents"></div>' +
     76           '<div class="bubble-close"></div>' +
     77           '<div class="bubble-shadow"></div>' +
     78           '<div class="bubble-arrow"></div>';
     79 
     80       this.hidden = true;
     81       this.handleCloseEvent = this.hide;
     82       this.deactivateToDismissDelay_ = 0;
     83       this.bubbleAlignment = BubbleAlignment.ARROW_TO_MID_ANCHOR;
     84     },
     85 
     86     /**
     87      * Sets the child node of the bubble.
     88      * @param {node} An HTML element
     89      */
     90     set content(node) {
     91       var bubbleContent = this.querySelector('.bubble-contents');
     92       bubbleContent.innerHTML = '';
     93       bubbleContent.appendChild(node);
     94     },
     95 
     96     /**
     97      * Handles close event which is triggered when the close button
     98      * is clicked. By default is set to this.hide.
     99      * @param {function} A function with no parameters
    100      */
    101     set handleCloseEvent(func) {
    102       this.handleCloseEvent_ = func;
    103     },
    104 
    105     /**
    106      * Sets the anchor node, i.e. the node that this bubble points at.
    107      * @param {HTMLElement} node The new anchor node.
    108      */
    109     set anchorNode(node) {
    110       this.anchorNode_ = node;
    111 
    112       if (!this.hidden)
    113         this.reposition();
    114     },
    115 
    116     /**
    117      * Sets the arrow location.
    118      * @param {cr.ui.ArrowLocation} arrowLocation The new arrow location.
    119      */
    120     setArrowLocation: function(arrowLocation) {
    121       this.isRight_ = arrowLocation == ArrowLocation.TOP_END ||
    122                       arrowLocation == ArrowLocation.BOTTOM_END;
    123       if (document.documentElement.dir == 'rtl')
    124         this.isRight_ = !this.isRight_;
    125       this.isTop_ = arrowLocation == ArrowLocation.TOP_START ||
    126                     arrowLocation == ArrowLocation.TOP_END;
    127 
    128       var bubbleArrow = this.querySelector('.bubble-arrow');
    129       bubbleArrow.setAttribute('is-right', this.isRight_);
    130       bubbleArrow.setAttribute('is-top', this.isTop_);
    131 
    132       if (!this.hidden)
    133         this.reposition();
    134     },
    135 
    136     /**
    137      * Sets the bubble alignment.
    138      * @param {cr.ui.BubbleAlignment} alignment The new bubble alignment.
    139      */
    140     set bubbleAlignment(alignment) {
    141       this.bubbleAlignment_ = alignment;
    142     },
    143 
    144     /**
    145      * Sets the delay before the user is allowed to click outside the bubble
    146      * to dismiss it. Using a delay makes it less likely that the user will
    147      * unintentionally dismiss the bubble.
    148      * @param {int} delay The delay in miliseconds.
    149      */
    150     set deactivateToDismissDelay(delay) {
    151       this.deactivateToDismissDelay_ = delay;
    152     },
    153 
    154     /**
    155      * Hides or shows the close button.
    156      * @param {Boolean} isVisible True if the close button should be visible.
    157      */
    158     setCloseButtonVisible: function(isVisible) {
    159       this.querySelector('.bubble-close').hidden = !isVisible;
    160     },
    161 
    162     /**
    163      * Updates the position of the bubble. This is automatically called when
    164      * the window is resized, but should also be called any time the layout
    165      * may have changed.
    166      */
    167     reposition: function() {
    168       var clientRect = this.anchorNode_.getBoundingClientRect();
    169 
    170       var left;
    171       if (this.bubbleAlignment_ ==
    172           BubbleAlignment.BUBBLE_EDGE_TO_ANCHOR_EDGE) {
    173         left = this.isRight_ ? clientRect.right - this.clientWidth :
    174             clientRect.left;
    175       } else {
    176         var anchorMid = (clientRect.left + clientRect.right) / 2;
    177         left = this.isRight_ ? anchorMid - this.clientWidth + ARROW_OFFSET_X :
    178             anchorMid - ARROW_OFFSET_X;
    179       }
    180       var top = this.isTop_ ? clientRect.bottom + ARROW_OFFSET_Y :
    181           clientRect.top - this.clientHeight - ARROW_OFFSET_Y;
    182 
    183       this.style.left = left + 'px';
    184       this.style.top = top + 'px';
    185     },
    186 
    187     /**
    188      * Starts showing the bubble. The bubble will show until the user clicks
    189      * away or presses Escape.
    190      */
    191     show: function() {
    192       if (!this.hidden)
    193         return;
    194 
    195       document.body.appendChild(this);
    196       this.hidden = false;
    197       this.reposition();
    198       this.showTime_ = Date.now();
    199 
    200       this.eventTracker_ = new EventTracker;
    201       this.eventTracker_.add(window, 'resize', this.reposition.bind(this));
    202 
    203       var doc = this.ownerDocument;
    204       this.eventTracker_.add(doc, 'keydown', this, true);
    205       this.eventTracker_.add(doc, 'mousedown', this, true);
    206     },
    207 
    208     /**
    209      * Hides the bubble from view.
    210      */
    211     hide: function() {
    212       this.hidden = true;
    213       this.eventTracker_.removeAll();
    214       this.parentNode.removeChild(this);
    215     },
    216 
    217     /**
    218      * Handles keydown and mousedown events, dismissing the bubble if
    219      * necessary.
    220      * @param {Event} e The event.
    221      */
    222     handleEvent: function(e) {
    223       switch (e.type) {
    224         case 'keydown': {
    225           if (e.keyCode == 27)  // Esc
    226             this.hide();
    227           break;
    228         }
    229         case 'mousedown': {
    230           if (e.target == this.querySelector('.bubble-close')) {
    231             this.handleCloseEvent_();
    232           } else if (!this.contains(e.target)) {
    233             if (Date.now() - this.showTime_ < this.deactivateToDismissDelay_)
    234               return;
    235             this.hide();
    236           } else {
    237             return;
    238           }
    239           break;
    240         }
    241       }
    242     },
    243   };
    244 
    245   return {
    246     ArrowLocation: ArrowLocation,
    247     Bubble: Bubble,
    248     BubbleAlignment: BubbleAlignment
    249   };
    250 });
    251