• 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/**
6 * @fileoverview Bubble implementation.
7 */
8
9// TODO(xiyuan): Move this into shared.
10cr.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