• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
7cr.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