1 // Copyright (C) 2019 Apple Inc. All rights reserved.
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions
6 // 1. Redistributions of source code must retain the above copyright
7 // notice, this list of conditions and the following disclaimer.
8 // 2. Redistributions in binary form must reproduce the above copyright
9 // notice, this list of conditions and the following disclaimer in the
10 // documentation and/or other materials provided with the distribution.
12 // THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS "AS IS"
13 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
14 // THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
15 // PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
16 // BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
17 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
18 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
19 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
20 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
21 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
22 // THE POSSIBILITY OF SUCH DAMAGE.
24 import {DOM, REF} from '/library/js/Ref.js';
26 function isPointInElement(element, point)
28 if (!element || element.style.display == 'none')
30 const bounds = element.getBoundingClientRect();
31 return point.x >= bounds.left && point.x <= bounds.right && point.y >= bounds.top && point.y <= bounds.bottom;
38 this.onArrowClick = null;
42 this.ref = REF.createRef({
43 state: {content: null, points: null},
44 onElementMount: (element) => {
45 element.addEventListener('mouseleave', (event) => {
46 if (!isPointInElement(self.arrow.element, event))
50 onStateUpdate: (element, stateDiff, state) => {
51 if (stateDiff.content) {
52 DOM.inject(element, stateDiff.content);
53 element.style.display = null;
55 if (!state.content && !element.style.display) {
56 element.style.display = 'none';
57 DOM.inject(element, '');
59 if (stateDiff.points) {
60 element.style.left = '0px';
61 element.style.top = '0px';
63 const upperPoint = stateDiff.points.length > 1 && stateDiff.points[0].y > stateDiff.points[1].y ? stateDiff.points[1] : stateDiff.points[0];
64 const lowerPoint = stateDiff.points.length > 1 && stateDiff.points[1].y > stateDiff.points[0].y ? stateDiff.points[1] : stateDiff.points[0];
65 const scrollDelta = document.documentElement.scrollTop || document.body.scrollTop;
66 const bounds = element.getBoundingClientRect();
67 const viewportWitdh = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
68 const viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
70 // Make an effort to place the tooltip in the center of the viewport.
71 let direction = 'down';
72 let tipY = upperPoint.y - 8 - bounds.height;
73 let point = upperPoint;
74 if (tipY < scrollDelta || tipY + bounds.height + (lowerPoint.y - upperPoint.y) / 2 < scrollDelta + viewportHeight / 2) {
76 tipY = lowerPoint.y + 16;
79 element.style.top = `${tipY}px`;
81 let tipX = point.x - bounds.width / 2;
82 if (tipX + bounds.width > viewportWitdh)
83 tipX = viewportWitdh - bounds.width;
86 element.style.left = `${tipX}px`;
88 self.arrow.setState({direction: direction, location: point});
92 this.arrow = REF.createRef({
93 state: {direction: null, location: null},
94 onElementMount: (element) => {
95 element.addEventListener('mouseleave', (event) => {
96 if (!isPointInElement(self.ref.element, event))
100 onStateUpdate: (element, stateDiff, state) => {
101 if (!state.direction || !state.location) {
102 element.style.display = 'none';
103 element.onclick = null;
104 element.style.cursor = null;
108 if (self.onArrowClick) {
109 element.onclick = self.onArrowClick;
110 element.style.cursor = 'pointer';
112 element.onclick = null;
113 element.style.cursor = null;
116 element.classList = [`tooltip arrow-${state.direction}`];
117 element.style.left = `${state.location.x - 15}px`;
118 if (state.direction == 'down')
119 element.style.top = `${state.location.y - 8}px`;
121 element.style.top = `${state.location.y - 13}px`;
122 element.style.display = null;
126 return `<div class="tooltip arrow-up" ref="${this.arrow}"></div>
127 <div class="tooltip-content" ref="${this.ref}">
130 set(content, points, onArrowClick = null) {
132 console.error('Cannot set ToolTip content, no tooltip on the page');
135 if (!points || points.length == 0) {
136 console.error('Tool tips require a location');
139 this.onArrowClick = onArrowClick;
140 this.ref.setState({content: content, points: points});
144 this.ref.setState({content: null, points: null});
146 this.arrow.setState({direction: null, points: null});
149 return isPointInElement(this.ref.element, point) || isPointInElement(this.arrow.element, point);
153 const ToolTip = new _ToolTip();