blob: 5f8fd7643efdc51e4070312998188e1394dd0087 [file] [log] [blame]
/*
* Copyright (C) 2009 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
export default class PopoverHelper {
/**
* @param {!Element} container
* @param {function(!MouseEvent):?UI.PopoverRequest} getRequest
*/
constructor(container, getRequest) {
this._disableOnClick = false;
this._hasPadding = false;
this._getRequest = getRequest;
this._scheduledRequest = null;
/** @type {?function()} */
this._hidePopoverCallback = null;
this._container = container;
this._showTimeout = 0;
this._hideTimeout = 0;
/** @type {?number} */
this._hidePopoverTimer = null;
/** @type {?number} */
this._showPopoverTimer = null;
this._boundMouseDown = this._mouseDown.bind(this);
this._boundMouseMove = this._mouseMove.bind(this);
this._boundMouseOut = this._mouseOut.bind(this);
this._container.addEventListener('mousedown', this._boundMouseDown, false);
this._container.addEventListener('mousemove', this._boundMouseMove, false);
this._container.addEventListener('mouseout', this._boundMouseOut, false);
this.setTimeout(1000);
}
/**
* @param {number} showTimeout
* @param {number=} hideTimeout
*/
setTimeout(showTimeout, hideTimeout) {
this._showTimeout = showTimeout;
this._hideTimeout = typeof hideTimeout === 'number' ? hideTimeout : showTimeout / 2;
}
/**
* @param {boolean} hasPadding
*/
setHasPadding(hasPadding) {
this._hasPadding = hasPadding;
}
/**
* @param {boolean} disableOnClick
*/
setDisableOnClick(disableOnClick) {
this._disableOnClick = disableOnClick;
}
/**
* @param {!Event} event
* @return {boolean}
*/
_eventInScheduledContent(event) {
return this._scheduledRequest ? this._scheduledRequest.box.contains(event.clientX, event.clientY) : false;
}
/**
* @param {!Event} event
*/
_mouseDown(event) {
if (this._disableOnClick) {
this.hidePopover();
return;
}
if (this._eventInScheduledContent(event)) {
return;
}
this._startHidePopoverTimer(0);
this._stopShowPopoverTimer();
this._startShowPopoverTimer(/** @type {!MouseEvent} */ (event), 0);
}
/**
* @param {!Event} event
*/
_mouseMove(event) {
// Pretend that nothing has happened.
if (this._eventInScheduledContent(event)) {
return;
}
this._startHidePopoverTimer(this._hideTimeout);
this._stopShowPopoverTimer();
if (event.which && this._disableOnClick) {
return;
}
this._startShowPopoverTimer(
/** @type {!MouseEvent} */ (event), this.isPopoverVisible() ? this._showTimeout * 0.6 : this._showTimeout);
}
/**
* @param {!Event} event
*/
_popoverMouseMove(event) {
this._stopHidePopoverTimer();
}
/**
* @param {!UI.GlassPane} popover
* @param {!Event} event
*/
_popoverMouseOut(popover, event) {
if (!popover.isShowing()) {
return;
}
if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(popover.contentElement)) {
this._startHidePopoverTimer(this._hideTimeout);
}
}
/**
* @param {!Event} event
*/
_mouseOut(event) {
if (!this.isPopoverVisible()) {
return;
}
if (!this._eventInScheduledContent(event)) {
this._startHidePopoverTimer(this._hideTimeout);
}
}
/**
* @param {number} timeout
*/
_startHidePopoverTimer(timeout) {
// User has |timeout| ms to reach the popup.
if (!this._hidePopoverCallback || this._hidePopoverTimer) {
return;
}
this._hidePopoverTimer = setTimeout(() => {
this._hidePopover();
this._hidePopoverTimer = null;
}, timeout);
}
/**
* @param {!MouseEvent} event
* @param {number} timeout
*/
_startShowPopoverTimer(event, timeout) {
this._scheduledRequest = this._getRequest.call(null, event);
if (!this._scheduledRequest) {
return;
}
this._showPopoverTimer = setTimeout(() => {
this._showPopoverTimer = null;
this._stopHidePopoverTimer();
this._hidePopover();
this._showPopover(event.target.ownerDocument);
}, timeout);
}
_stopShowPopoverTimer() {
if (!this._showPopoverTimer) {
return;
}
clearTimeout(this._showPopoverTimer);
this._showPopoverTimer = null;
}
/**
* @return {boolean}
*/
isPopoverVisible() {
return !!this._hidePopoverCallback;
}
hidePopover() {
this._stopShowPopoverTimer();
this._hidePopover();
}
_hidePopover() {
if (!this._hidePopoverCallback) {
return;
}
this._hidePopoverCallback.call(null);
this._hidePopoverCallback = null;
}
/**
* @param {!Document} document
*/
_showPopover(document) {
const popover = new UI.GlassPane();
popover.registerRequiredCSS('ui/popover.css');
popover.setSizeBehavior(UI.GlassPane.SizeBehavior.MeasureContent);
popover.setMarginBehavior(UI.GlassPane.MarginBehavior.Arrow);
const request = this._scheduledRequest;
request.show.call(null, popover).then(success => {
if (!success) {
return;
}
if (this._scheduledRequest !== request) {
if (request.hide) {
request.hide.call(null);
}
return;
}
// This should not happen, but we hide previous popover to be on the safe side.
if (PopoverHelper._popoverHelper) {
console.error('One popover is already visible');
PopoverHelper._popoverHelper.hidePopover();
}
PopoverHelper._popoverHelper = this;
popover.contentElement.classList.toggle('has-padding', this._hasPadding);
popover.contentElement.addEventListener('mousemove', this._popoverMouseMove.bind(this), true);
popover.contentElement.addEventListener('mouseout', this._popoverMouseOut.bind(this, popover), true);
popover.setContentAnchorBox(request.box);
popover.show(document);
this._hidePopoverCallback = () => {
if (request.hide) {
request.hide.call(null);
}
popover.hide();
delete PopoverHelper._popoverHelper;
};
});
}
_stopHidePopoverTimer() {
if (!this._hidePopoverTimer) {
return;
}
clearTimeout(this._hidePopoverTimer);
this._hidePopoverTimer = null;
// We know that we reached the popup, but we might have moved over other elements.
// Discard pending command.
this._stopShowPopoverTimer();
}
dispose() {
this._container.removeEventListener('mousedown', this._boundMouseDown, false);
this._container.removeEventListener('mousemove', this._boundMouseMove, false);
this._container.removeEventListener('mouseout', this._boundMouseOut, false);
}
}
/** @typedef {{box: !AnchorBox, show:(function(!UI.GlassPane):!Promise<boolean>), hide:(function()|undefined)}} */
UI.PopoverRequest;
/* Legacy exported object*/
self.UI = self.UI || {};
/* Legacy exported object*/
UI = UI || {};
/** @constructor */
UI.PopoverHelper = PopoverHelper;