blob: 92bf987d51df2430851db0e739994ac8614cacf5 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Bubble implementation.
*/
// TODO(xiyuan): Move this into shared.
cr.define('cr.ui', function() {
/**
* Creates a bubble div.
* @constructor
* @extends {HTMLDivElement}
*/
var Bubble = cr.ui.define('div');
/**
* Bubble key codes.
* @enum {number}
*/
var KeyCodes = {
TAB: 9,
ENTER: 13,
ESC: 27,
SPACE: 32
};
/**
* Bubble attachment side.
* @enum {string}
*/
Bubble.Attachment = {
RIGHT: 'bubble-right',
LEFT: 'bubble-left',
TOP: 'bubble-top',
BOTTOM: 'bubble-bottom'
};
Bubble.prototype = {
__proto__: HTMLDivElement.prototype,
// Anchor element for this bubble.
anchor_: undefined,
// If defined, sets focus to this element once bubble is closed. Focus is
// set to this element only if there's no any other focused element.
elementToFocusOnHide_: undefined,
// With help of these elements we create closed artificial tab-cycle through
// bubble elements.
firstBubbleElement_: undefined,
lastBubbleElement_: undefined,
// Whether to hide bubble when key is pressed.
hideOnKeyPress_: true,
/** @override */
decorate: function() {
this.docKeyDownHandler_ = this.handleDocKeyDown_.bind(this);
this.selfClickHandler_ = this.handleSelfClick_.bind(this);
this.ownerDocument.addEventListener('click',
this.handleDocClick_.bind(this));
this.ownerDocument.addEventListener('keydown',
this.docKeyDownHandler_);
window.addEventListener('blur', this.handleWindowBlur_.bind(this));
this.addEventListener('webkitTransitionEnd',
this.handleTransitionEnd_.bind(this));
// Guard timer for 200ms + epsilon.
ensureTransitionEndEvent(this, 250);
},
/**
* Element that should be focused on hide.
* @type {HTMLElement}
*/
set elementToFocusOnHide(value) {
this.elementToFocusOnHide_ = value;
},
/**
* Element that should be focused on shift-tab of first bubble element
* to create artificial closed tab-cycle through bubble.
* Usually close-button.
* @type {HTMLElement}
*/
set lastBubbleElement(value) {
this.lastBubbleElement_ = value;
},
/**
* Element that should be focused on tab of last bubble element
* to create artificial closed tab-cycle through bubble.
* Same element as first focused on bubble opening.
* @type {HTMLElement}
*/
set firstBubbleElement(value) {
this.firstBubbleElement_ = value;
},
/**
* Whether to hide bubble when key is pressed.
* @type {boolean}
*/
set hideOnKeyPress(value) {
this.hideOnKeyPress_ = value;
},
/**
* Whether to hide bubble when clicked inside bubble element.
* Default is true.
* @type {boolean}
*/
set hideOnSelfClick(value) {
if (value)
this.removeEventListener('click', this.selfClickHandler_);
else
this.addEventListener('click', this.selfClickHandler_);
},
/**
* Handler for click event which prevents bubble auto hide.
* @private
*/
handleSelfClick_: function(e) {
// Allow clicking on [x] button.
if (e.target && e.target.classList.contains('close-button'))
return;
e.stopPropagation();
},
/**
* Sets the attachment of the bubble.
* @param {!Attachment} attachment Bubble attachment.
*/
setAttachment_: function(attachment) {
for (var k in Bubble.Attachment) {
var v = Bubble.Attachment[k];
this.classList.toggle(v, v == attachment);
}
},
/**
* Shows the bubble for given anchor element.
* @param {!Object} pos Bubble position (left, top, right, bottom in px).
* @param {!Attachment} attachment Bubble attachment (on which side of the
* specified position it should be displayed).
* @param {HTMLElement} opt_content Content to show in bubble.
* If not specified, bubble element content is shown.
* @private
*/
showContentAt_: function(pos, attachment, opt_content) {
this.style.top = this.style.left = this.style.right = this.style.bottom =
'auto';
for (var k in pos) {
if (typeof pos[k] == 'number')
this.style[k] = pos[k] + 'px';
}
if (opt_content !== undefined) {
this.innerHTML = '';
this.appendChild(opt_content);
}
this.setAttachment_(attachment);
this.hidden = false;
this.classList.remove('faded');
},
/**
* Shows the bubble for given anchor element. Bubble content is not cleared.
* @param {!HTMLElement} el Anchor element of the bubble.
* @param {!Attachment} attachment Bubble attachment (on which side of the
* element it should be displayed).
* @param {number=} opt_offset Offset of the bubble.
* @param {number=} opt_padding Optional padding of the bubble.
*/
showForElement: function(el, attachment, opt_offset, opt_padding) {
this.showContentForElement(
el, attachment, undefined, opt_offset, opt_padding);
},
/**
* Shows the bubble for given anchor element.
* @param {!HTMLElement} el Anchor element of the bubble.
* @param {!Attachment} attachment Bubble attachment (on which side of the
* element it should be displayed).
* @param {HTMLElement} opt_content Content to show in bubble.
* If not specified, bubble element content is shown.
* @param {number=} opt_offset Offset of the bubble attachment point from
* left (for vertical attachment) or top (for horizontal attachment)
* side of the element. If not specified, the bubble is positioned to
* be aligned with the left/top side of the element but not farther than
* half of its width/height.
* @param {number=} opt_padding Optional padding of the bubble.
*/
showContentForElement: function(el, attachment, opt_content,
opt_offset, opt_padding) {
/** @const */ var ARROW_OFFSET = 25;
/** @const */ var DEFAULT_PADDING = 18;
if (opt_padding == undefined)
opt_padding = DEFAULT_PADDING;
var origin = cr.ui.login.DisplayManager.getPosition(el);
var offset = opt_offset == undefined ?
[Math.min(ARROW_OFFSET, el.offsetWidth / 2),
Math.min(ARROW_OFFSET, el.offsetHeight / 2)] :
[opt_offset, opt_offset];
var pos = {};
if (isRTL()) {
switch (attachment) {
case Bubble.Attachment.TOP:
pos.right = origin.right + offset[0] - ARROW_OFFSET;
pos.bottom = origin.bottom + el.offsetHeight + opt_padding;
break;
case Bubble.Attachment.RIGHT:
pos.top = origin.top + offset[1] - ARROW_OFFSET;
pos.right = origin.right + el.offsetWidth + opt_padding;
break;
case Bubble.Attachment.BOTTOM:
pos.right = origin.right + offset[0] - ARROW_OFFSET;
pos.top = origin.top + el.offsetHeight + opt_padding;
break;
case Bubble.Attachment.LEFT:
pos.top = origin.top + offset[1] - ARROW_OFFSET;
pos.left = origin.left + el.offsetWidth + opt_padding;
break;
}
} else {
switch (attachment) {
case Bubble.Attachment.TOP:
pos.left = origin.left + offset[0] - ARROW_OFFSET;
pos.bottom = origin.bottom + el.offsetHeight + opt_padding;
break;
case Bubble.Attachment.RIGHT:
pos.top = origin.top + offset[1] - ARROW_OFFSET;
pos.left = origin.left + el.offsetWidth + opt_padding;
break;
case Bubble.Attachment.BOTTOM:
pos.left = origin.left + offset[0] - ARROW_OFFSET;
pos.top = origin.top + el.offsetHeight + opt_padding;
break;
case Bubble.Attachment.LEFT:
pos.top = origin.top + offset[1] - ARROW_OFFSET;
pos.right = origin.right + el.offsetWidth + opt_padding;
break;
}
}
this.anchor_ = el;
this.showContentAt_(pos, attachment, opt_content);
},
/**
* Shows the bubble for given anchor element.
* @param {!HTMLElement} el Anchor element of the bubble.
* @param {string} text Text content to show in bubble.
* @param {!Attachment} attachment Bubble attachment (on which side of the
* element it should be displayed).
* @param {number=} opt_offset Offset of the bubble attachment point from
* left (for vertical attachment) or top (for horizontal attachment)
* side of the element. If not specified, the bubble is positioned to
* be aligned with the left/top side of the element but not farther than
* half of its weight/height.
* @param {number=} opt_padding Optional padding of the bubble.
*/
showTextForElement: function(el, text, attachment,
opt_offset, opt_padding) {
var span = this.ownerDocument.createElement('span');
span.textContent = text;
this.showContentForElement(el, attachment, span, opt_offset, opt_padding);
},
/**
* Hides the bubble.
*/
hide: function() {
if (!this.classList.contains('faded'))
this.classList.add('faded');
},
/**
* Hides the bubble anchored to the given element (if any).
* @param {!Object} el Anchor element.
*/
hideForElement: function(el) {
if (!this.hidden && this.anchor_ == el)
this.hide();
},
/**
* Handler for faded transition end.
* @private
*/
handleTransitionEnd_: function(e) {
if (this.classList.contains('faded')) {
this.hidden = true;
if (this.elementToFocusOnHide_)
this.elementToFocusOnHide_.focus();
}
},
/**
* Handler of document click event.
* @private
*/
handleDocClick_: function(e) {
// Ignore clicks on anchor element.
if (e.target == this.anchor_)
return;
if (!this.hidden)
this.hide();
},
/**
* Handle of document keydown event.
* @private
*/
handleDocKeyDown_: function(e) {
if (this.hidden)
return;
if (this.hideOnKeyPress_) {
this.hide();
return;
}
// Artificial tab-cycle.
if (e.keyCode == KeyCodes.TAB && e.shiftKey == true &&
e.target == this.firstBubbleElement_) {
this.lastBubbleElement_.focus();
e.preventDefault();
}
if (e.keyCode == KeyCodes.TAB && e.shiftKey == false &&
e.target == this.lastBubbleElement_) {
this.firstBubbleElement_.focus();
e.preventDefault();
}
// Close bubble on ESC or on hitting spacebar or Enter at close-button.
if (e.keyCode == KeyCodes.ESC ||
((e.keyCode == KeyCodes.ENTER || e.keyCode == KeyCodes.SPACE) &&
e.target && e.target.classList.contains('close-button')))
this.hide();
},
/**
* Handler of window blur event.
* @private
*/
handleWindowBlur_: function(e) {
if (!this.hidden)
this.hide();
}
};
return {
Bubble: Bubble
};
});