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 - 1 && point.x <= bounds.right + 1 && point.y >= bounds.top - 1 && point.y <= bounds.bottom + 1;
38 this.onArrowClick = null;
45 this.ref = REF.createRef({
46 state: {content: null, points: null},
47 onElementMount: (element) => {
48 element.addEventListener('mouseleave', (event) => {
49 if (element.style.display === 'none')
51 if (!isPointInElement(self.arrow.element, event))
55 onStateUpdate: (element, stateDiff, state) => {
56 if (stateDiff.content) {
57 DOM.inject(element, stateDiff.content);
58 element.style.display = null;
60 element.style.display = 'none';
61 DOM.inject(element, '');
64 if (stateDiff.points) {
65 element.style.left = '0px';
66 element.style.top = '0px';
68 const upperPoint = stateDiff.points.length > 1 && stateDiff.points[0].y > stateDiff.points[1].y ? stateDiff.points[1] : stateDiff.points[0];
69 const lowerPoint = stateDiff.points.length > 1 && stateDiff.points[1].y > stateDiff.points[0].y ? stateDiff.points[1] : stateDiff.points[0];
70 const scrollDelta = document.documentElement.scrollTop || document.body.scrollTop;
71 const bounds = element.getBoundingClientRect();
72 const viewportWitdh = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
73 const viewportHeight = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
75 let direction = 'down';
76 let point = upperPoint;
78 if (upperPoint.y == lowerPoint.y) {
80 const leftPoint = stateDiff.points.length > 1 && stateDiff.points[0].x > stateDiff.points[1].x ? stateDiff.points[1] : stateDiff.points[0];
81 const rightPoint = stateDiff.points.length > 1 && stateDiff.points[1].x > stateDiff.points[0].x ? stateDiff.points[1] : stateDiff.points[0];
84 let tipX = leftPoint.x - 12 - bounds.width;
86 if (tipX < 0 || tipX + bounds.width + (rightPoint.x - leftPoint.x) / 2 < viewportWitdh / 2) {
88 tipX = rightPoint.x + 16;
91 element.style.left = `${tipX}px`;
93 let tipY = point.y - bounds.height / 2;
94 if (tipY + bounds.height > scrollDelta + viewportHeight)
95 tipY = scrollDelta + viewportHeight - bounds.height;
98 element.style.top = `${tipY}px`;
100 // Make an effort to place the tooltip in the center of the viewport.
101 let tipY = upperPoint.y - 8 - bounds.height;
103 if (tipY < scrollDelta || tipY + bounds.height + (lowerPoint.y - upperPoint.y) / 2 < scrollDelta + viewportHeight / 2) {
105 tipY = lowerPoint.y + 16;
108 element.style.top = `${tipY}px`;
110 let tipX = point.x - bounds.width / 2;
111 if (tipX + bounds.width > viewportWitdh)
112 tipX = viewportWitdh - bounds.width;
115 element.style.left = `${tipX}px`;
118 self.arrow.setState({direction: direction, location: point});
122 this.arrow = REF.createRef({
123 state: {direction: null, location: null},
124 onElementMount: (element) => {
125 element.addEventListener('mouseleave', (event) => {
126 if (element.style.display === 'none')
128 if (!isPointInElement(self.ref.element, event) && !isPointInElement(element, event))
132 onStateUpdate: (element, stateDiff) => {
133 if (!stateDiff.direction || !stateDiff.location) {
134 element.style.display = 'none';
135 element.onclick = null;
136 element.style.cursor = null;
140 if (self.onArrowClick) {
141 element.onclick = self.onArrowClick;
142 element.style.cursor = 'pointer';
144 element.onclick = null;
145 element.style.cursor = null;
148 element.classList = [`tooltip arrow-${stateDiff.direction}`];
150 if (stateDiff.direction == 'down') {
151 element.style.left = `${stateDiff.location.x - 15}px`;
152 element.style.top = `${stateDiff.location.y - 8}px`;
153 } else if (stateDiff.direction == 'left') {
154 element.style.left = `${stateDiff.location.x - 30}px`;
155 element.style.top = `${stateDiff.location.y - 15}px`;
156 } else if (stateDiff.direction == 'right') {
157 element.style.left = `${stateDiff.location.x - 13}px`;
158 element.style.top = `${stateDiff.location.y - 15}px`;
160 element.style.left = `${stateDiff.location.x - 15}px`;
161 element.style.top = `${stateDiff.location.y - 13}px`;
163 element.style.display = null;
167 return `<div class="tooltip arrow-up" ref="${this.arrow}"></div>
168 <div class="tooltip-content" ref="${this.ref}">
171 set(content, points, onArrowClick = null) {
173 console.error('Cannot set ToolTip content, no tooltip on the page');
176 if (!points || points.length == 0) {
177 console.error('Tool tips require a location');
180 this.onArrowClick = onArrowClick;
181 this.ref.setState({content: content, points: points});
183 setByElement(content, element, options) {
184 const bound = element.getBoundingClientRect();
185 const orientation = options.orientation ? options.orientation : this.VERTICAL;
186 const onArrowClick = options.onArrowClick ? options.onArrowClick : null;
188 // Manage the scroll delta
190 if (window.getComputedStyle(element.offsetParent).getPropertyValue('position') == 'fixed')
191 scrollDelta = document.documentElement.scrollTop || document.body.scrollTop;
193 if (options.orientation) {
195 {x: bound.right, y: (bound.top + bound.bottom) / 2 + scrollDelta},
196 {x: bound.left, y: (bound.top + bound.bottom) / 2 + scrollDelta},
200 {x: (bound.right + bound.left) / 2, y: bound.top + scrollDelta},
201 {x: (bound.right + bound.left) / 2, y: bound.bottom + scrollDelta},
207 this.ref.setState({content: null, points: null});
209 this.arrow.setState({direction: null, points: null});
212 return isPointInElement(this.ref.element, point) || isPointInElement(this.arrow.element, point);
216 const ToolTip = new _ToolTip();