blob: 8b27faabd46f5deb9e05d9dc96d2e04773a73dd8 [file] [log] [blame]
// Copyright (C) 2019 Apple 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:
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
// 2. 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.
//
// THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS 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 APPLE INC. OR ITS 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.
import {DOM, REF} from '/library/js/Ref.js';
function isPointInElement(element, point)
{
if (!element || element.style.display == 'none')
return false;
const bounds = element.getBoundingClientRect();
return point.x >= bounds.left - 1 && point.x <= bounds.right + 1 && point.y >= bounds.top - 1 && point.y <= bounds.bottom + 1;
}
class _ToolTip {
constructor() {
this.ref = null;
this.arrow = null;
this.onArrowClick = null;
this.VERTICAL = 0;
this.HORIZONTAL = 1;
}
toString() {
const self = this;
this.ref = REF.createRef({
state: {content: null, points: null, viewport: null},
onElementMount: (element) => {
element.addEventListener('mouseleave', (event) => {
if (element.style.display === 'none')
return;
if (!isPointInElement(self.arrow.element, event))
this.unset()
});
},
onStateUpdate: (element, stateDiff, state) => {
if (stateDiff.content) {
DOM.inject(element, stateDiff.content);
element.style.display = null;
} else {
element.style.display = 'none';
DOM.inject(element, '');
}
if (stateDiff.points || stateDiff.viewport) {
element.style.left = '0px';
element.style.top = '0px';
const upperPoint = stateDiff.points.length > 1 && stateDiff.points[0].y > stateDiff.points[1].y ? stateDiff.points[1] : stateDiff.points[0];
const lowerPoint = stateDiff.points.length > 1 && stateDiff.points[1].y > stateDiff.points[0].y ? stateDiff.points[1] : stateDiff.points[0];
const scrollDelta = document.documentElement.scrollTop || document.body.scrollTop;
const bounds = element.getBoundingClientRect();
let direction = 'down';
let point = upperPoint;
if (upperPoint.y == lowerPoint.y) {
// Horizontal tooltip
const leftPoint = stateDiff.points.length > 1 && stateDiff.points[0].x > stateDiff.points[1].x ? stateDiff.points[1] : stateDiff.points[0];
const rightPoint = stateDiff.points.length > 1 && stateDiff.points[1].x > stateDiff.points[0].x ? stateDiff.points[1] : stateDiff.points[0];
direction = 'left';
let tipX = leftPoint.x - 12 - bounds.width;
point = rightPoint;
if (tipX < 0 || tipX + bounds.width + (rightPoint.x - leftPoint.x) / 2 < stateDiff.viewport.x + stateDiff.viewport.width / 2) {
direction = 'right';
tipX = rightPoint.x + 16;
point = rightPoint;
}
element.style.left = `${tipX}px`;
let tipY = point.y - bounds.height / 2;
if (tipY + bounds.height > scrollDelta + stateDiff.viewport.y + stateDiff.viewport.height)
tipY = scrollDelta + stateDiff.viewport.y + stateDiff.viewport.height - bounds.height;
if (tipY < 0)
tipY = 0;
element.style.top = `${tipY}px`;
} else {
// Make an effort to place the tooltip in the center of the viewport.
let tipY = upperPoint.y - 8 - bounds.height;
point = upperPoint;
if (tipY < scrollDelta || tipY + bounds.height + (lowerPoint.y - upperPoint.y) / 2 < scrollDelta + stateDiff.viewport.y + stateDiff.viewport.height / 2) {
direction = 'up';
tipY = lowerPoint.y + 16;
point = lowerPoint;
}
element.style.top = `${tipY}px`;
let tipX = point.x - bounds.width / 2;
if (tipX + bounds.width > stateDiff.viewport.x + stateDiff.viewport.width)
tipX = stateDiff.viewport.x + stateDiff.viewport.width - bounds.width;
if (tipX < 0)
tipX = 0;
element.style.left = `${tipX}px`;
}
self.arrow.setState({direction: direction, location: point});
}
},
});
this.arrow = REF.createRef({
state: {direction: null, location: null},
onElementMount: (element) => {
element.addEventListener('mouseleave', (event) => {
if (element.style.display === 'none')
return;
if (!isPointInElement(self.ref.element, event) && !isPointInElement(element, event))
this.unset()
});
},
onStateUpdate: (element, stateDiff) => {
if (!stateDiff.direction || !stateDiff.location) {
element.style.display = 'none';
element.onclick = null;
element.style.cursor = null;
return;
}
if (self.onArrowClick) {
element.onclick = self.onArrowClick;
element.style.cursor = 'pointer';
} else {
element.onclick = null;
element.style.cursor = null;
}
element.classList = [`tooltip arrow-${stateDiff.direction}`];
if (stateDiff.direction == 'down') {
element.style.left = `${stateDiff.location.x - 15}px`;
element.style.top = `${stateDiff.location.y - 8}px`;
} else if (stateDiff.direction == 'left') {
element.style.left = `${stateDiff.location.x - 30}px`;
element.style.top = `${stateDiff.location.y - 15}px`;
} else if (stateDiff.direction == 'right') {
element.style.left = `${stateDiff.location.x - 13}px`;
element.style.top = `${stateDiff.location.y - 15}px`;
} else {
element.style.left = `${stateDiff.location.x - 15}px`;
element.style.top = `${stateDiff.location.y - 13}px`;
}
element.style.display = null;
},
});
return `<div class="tooltip arrow-up" ref="${this.arrow}"></div>
<div class="tooltip-content" ref="${this.ref}">
</div>`;
}
set(content, points, onArrowClick = null, viewport = null) {
if (!this.ref) {
console.error('Cannot set ToolTip content, no tooltip on the page');
return;
}
if (!points || points.length == 0) {
console.error('Tool tips require a location');
return;
}
this.onArrowClick = onArrowClick;
const windowWidth = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
const windowHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
if (!viewport)
viewport = {
x: 0,
y: 0,
width: windowWidth,
height: windowHeight,
}
else {
let rect = viewport.getBoundingClientRect();
viewport = {
x: 0,
y: 0,
width: Math.min(windowWidth, rect.width),
height: Math.min(windowHeight, rect.height),
};
}
this.ref.setState({content: content, points: points, viewport: viewport});
}
setByElement(content, element, options, viewport = null) {
const bound = element.getBoundingClientRect();
const orientation = options.orientation ? options.orientation : this.VERTICAL;
const onArrowClick = options.onArrowClick ? options.onArrowClick : null;
// Manage the scroll delta
let scrollDelta = 0;
if (window.getComputedStyle(element.offsetParent).getPropertyValue('position') == 'fixed')
scrollDelta = document.documentElement.scrollTop || document.body.scrollTop;
if (options.orientation) {
this.set(content, [
{x: bound.right, y: (bound.top + bound.bottom) / 2 + scrollDelta},
{x: bound.left, y: (bound.top + bound.bottom) / 2 + scrollDelta},
], onArrowClick, viewport);
} else {
this.set(content, [
{x: (bound.right + bound.left) / 2, y: bound.top + scrollDelta},
{x: (bound.right + bound.left) / 2, y: bound.bottom + scrollDelta},
], onArrowClick, viewport);
}
}
unset() {
if (this.ref)
this.ref.setState({content: null, points: null});
if (this.arrow)
this.arrow.setState({direction: null, points: null});
}
isIn(point) {
return isPointInElement(this.ref.element, point) || isPointInElement(this.arrow.element, point);
}
}
const ToolTip = new _ToolTip();
export {ToolTip};