photo-sphere-viewer
Version:
A JavaScript library to display Photo Sphere panoramas
399 lines (351 loc) • 10.9 kB
JavaScript
import { EVENTS } from '../data/constants';
import { PSVError } from '../PSVError';
import { addClasses, cleanPosition, positionIsOrdered } from '../utils';
import { AbstractComponent } from './AbstractComponent';
const STATE = { NONE: 0, SHOWING: 1, HIDING: 2, READY: 3 };
/**
* @typedef {Object} PSV.components.Tooltip.Position
* @summary Object defining the tooltip position
* @property {number} top - Position of the tip of the arrow of the tooltip, in pixels
* @property {number} left - Position of the tip of the arrow of the tooltip, in pixels
* @property {string|string[]} [position='top center'] - Tooltip position toward it's arrow tip.
* Accepted values are combinations of `top`, `center`, `bottom` and `left`, `center`, `right`
* @property {Object} [box] - Used when displaying a tooltip on a marker
* @property {number} [box.width=0]
* @property {number} [box.height=0]
*/
/**
* @typedef {PSV.components.Tooltip.Position} PSV.components.Tooltip.Config
* @summary Object defining the tooltip configuration
* @property {string} content - HTML content of the tooltip
* @property {string} [className] - Additional CSS class added to the tooltip
* @property {*} [data] - Userdata associated to the tooltip
*/
/**
* @summary Tooltip component
* @description Never instanciate tooltips directly use {@link PSV.services.TooltipRenderer} instead
* @extends PSV.components.AbstractComponent
* @memberof PSV.components
*/
export class Tooltip extends AbstractComponent {
/**
* @param {PSV.Viewer} psv
* @param {{arrow: number, border: number}} size
*/
constructor(psv, size) {
super(psv, 'psv-tooltip');
/**
* @override
* @property {number} arrow
* @property {number} border
* @property {number} width
* @property {number} height
* @property {string} pos
* @property {string} state
* @property {*} data
*/
this.prop = {
...this.prop,
...size,
state : STATE.NONE,
width : 0,
height: 0,
pos : '',
config: null,
data : null,
};
/**
* Tooltip content
* @member {HTMLElement}
* @readonly
* @private
*/
this.content = document.createElement('div');
this.content.className = 'psv-tooltip-content';
this.container.appendChild(this.content);
/**
* Tooltip arrow
* @member {HTMLElement}
* @readonly
* @package
*/
this.arrow = document.createElement('div');
this.arrow.className = 'psv-tooltip-arrow';
this.container.appendChild(this.arrow);
this.container.addEventListener('transitionend', this);
this.container.style.top = '-1000px';
this.container.style.left = '-1000px';
}
/**
* @override
*/
destroy() {
delete this.arrow;
delete this.content;
super.destroy();
}
/**
* @summary Handles events
* @param {Event} e
* @private
*/
handleEvent(e) {
/* eslint-disable */
switch (e.type) {
// @formatter:off
case 'transitionend': this.__onTransitionEnd(e); break;
// @formatter:on
}
/* eslint-enable */
}
/**
* @override
* @summary This method is not supported
* @throws {PSV.PSVError} always
*/
toggle() {
throw new PSVError('Tooltip cannot be toggled');
}
/**
* @summary Displays the tooltip on the viewer
* Do not call this method directly, use {@link PSV.services.TooltipRenderer} instead.
* @param {PSV.components.Tooltip.Config} config
*
* @fires PSV.show-tooltip
* @throws {PSV.PSVError} when the configuration is incorrect
*
* @package
*/
show(config) {
if (this.prop.state !== STATE.NONE) {
throw new PSVError('Initialized tooltip cannot be re-initialized');
}
if (config.className) {
addClasses(this.container, config.className);
}
this.content.innerHTML = config.content;
const rect = this.container.getBoundingClientRect();
this.prop.width = rect.right - rect.left;
this.prop.height = rect.bottom - rect.top;
this.prop.state = STATE.READY;
this.move(config);
this.prop.data = config.data;
this.prop.state = STATE.SHOWING;
this.psv.trigger(EVENTS.SHOW_TOOLTIP, this.prop.data, this);
this.__waitImages();
}
/**
* @summary Moves the tooltip to a new position
* @param {PSV.components.Tooltip.Position} config
*
* @throws {PSV.PSVError} when the configuration is incorrect
*/
move(config) {
if (this.prop.state !== STATE.SHOWING && this.prop.state !== STATE.READY) {
throw new PSVError('Uninitialized tooltip cannot be moved');
}
if (!config.box) {
config.box = {
width : 0,
height: 0,
};
}
this.config = config;
const t = this.container;
const a = this.arrow;
// compute size
const style = {
posClass : cleanPosition(config.position, { allowCenter: false, cssOrder: false }) || ['top', 'center'],
width : this.prop.width,
height : this.prop.height,
top : 0,
left : 0,
arrowTop : 0,
arrowLeft: 0,
};
// set initial position
this.__computeTooltipPosition(style, config);
// correct position if overflow
let swapY = null;
let swapX = null;
if (style.top < 0) {
swapY = 'bottom';
}
else if (style.top + style.height > this.psv.prop.size.height) {
swapY = 'top';
}
if (style.left < 0) {
swapX = 'right';
}
else if (style.left + style.width > this.psv.prop.size.width) {
swapX = 'left';
}
if (swapX || swapY) {
const ordered = positionIsOrdered(style.posClass);
if (swapY) {
style.posClass[ordered ? 0 : 1] = swapY;
}
if (swapX) {
style.posClass[ordered ? 1 : 0] = swapX;
}
this.__computeTooltipPosition(style, config);
}
// apply position
t.style.top = style.top + 'px';
t.style.left = style.left + 'px';
a.style.top = style.arrowTop + 'px';
a.style.left = style.arrowLeft + 'px';
const newPos = style.posClass.join('-');
if (newPos !== this.prop.pos) {
t.classList.remove(`psv-tooltip--${this.prop.pos}`);
this.prop.pos = newPos;
t.classList.add(`psv-tooltip--${this.prop.pos}`);
}
}
/**
* @summary Hides the tooltip
* @fires PSV.hide-tooltip
*/
hide() {
this.container.classList.remove('psv-tooltip--visible');
this.prop.state = STATE.HIDING;
this.psv.trigger(EVENTS.HIDE_TOOLTIP, this.prop.data);
}
/**
* @summary Finalize transition
* @param {TransitionEvent} e
* @private
*/
__onTransitionEnd(e) {
if (e.propertyName === 'transform') {
switch (this.prop.state) {
case STATE.SHOWING:
this.container.classList.add('psv-tooltip--visible');
this.prop.state = STATE.READY;
break;
case STATE.HIDING:
this.prop.state = STATE.NONE;
this.destroy();
break;
default:
// nothing
}
}
}
/**
* @summary Computes the position of the tooltip and its arrow
* @param {Object} style
* @param {Object} config
* @private
*/
__computeTooltipPosition(style, config) {
const arrow = this.prop.arrow;
const top = config.top;
const height = style.height;
const left = config.left;
const width = style.width;
const offsetSide = arrow + this.prop.border;
const offsetX = config.box.width / 2 + arrow * 2;
const offsetY = config.box.height / 2 + arrow * 2;
switch (style.posClass.join('-')) {
case 'top-left':
style.top = top - offsetY - height;
style.left = left + offsetSide - width;
style.arrowTop = height;
style.arrowLeft = width - offsetSide - arrow;
break;
case 'top-center':
style.top = top - offsetY - height;
style.left = left - width / 2;
style.arrowTop = height;
style.arrowLeft = width / 2 - arrow;
break;
case 'top-right':
style.top = top - offsetY - height;
style.left = left - offsetSide;
style.arrowTop = height;
style.arrowLeft = arrow;
break;
case 'bottom-left':
style.top = top + offsetY;
style.left = left + offsetSide - width;
style.arrowTop = -arrow * 2;
style.arrowLeft = width - offsetSide - arrow;
break;
case 'bottom-center':
style.top = top + offsetY;
style.left = left - width / 2;
style.arrowTop = -arrow * 2;
style.arrowLeft = width / 2 - arrow;
break;
case 'bottom-right':
style.top = top + offsetY;
style.left = left - offsetSide;
style.arrowTop = -arrow * 2;
style.arrowLeft = arrow;
break;
case 'left-top':
style.top = top + offsetSide - height;
style.left = left - offsetX - width;
style.arrowTop = height - offsetSide - arrow;
style.arrowLeft = width;
break;
case 'center-left':
style.top = top - height / 2;
style.left = left - offsetX - width;
style.arrowTop = height / 2 - arrow;
style.arrowLeft = width;
break;
case 'left-bottom':
style.top = top - offsetSide;
style.left = left - offsetX - width;
style.arrowTop = arrow;
style.arrowLeft = width;
break;
case 'right-top':
style.top = top + offsetSide - height;
style.left = left + offsetX;
style.arrowTop = height - offsetSide - arrow;
style.arrowLeft = -arrow * 2;
break;
case 'center-right':
style.top = top - height / 2;
style.left = left + offsetX;
style.arrowTop = height / 2 - arrow;
style.arrowLeft = -arrow * 2;
break;
case 'right-bottom':
style.top = top - offsetSide;
style.left = left + offsetX;
style.arrowTop = arrow;
style.arrowLeft = -arrow * 2;
break;
// no default
}
}
/**
* @summary If the tooltip contains images, recompute its size once they are loaded
* @private
*/
__waitImages() {
const images = this.content.querySelectorAll('img');
if (images.length > 0) {
const promises = [];
images.forEach((image) => {
promises.push(new Promise((resolve) => {
image.onload = resolve;
image.onerror = resolve;
}));
});
Promise.all(promises)
.then(() => {
if (this.prop.state === STATE.SHOWING || this.prop.state === STATE.READY) {
const rect = this.container.getBoundingClientRect();
this.prop.width = rect.right - rect.left;
this.prop.height = rect.bottom - rect.top;
this.move(this.config);
}
});
}
}
}