results.webkit.org: Move legend into sidebar
[WebKit-https.git] / Tools / resultsdbpy / resultsdbpy / view / static / js / tooltip.js
1 // Copyright (C) 2019 Apple Inc. All rights reserved.
2 //
3 // Redistribution and use in source and binary forms, with or without
4 // modification, are permitted provided that the following conditions
5 // are met:
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.
11 //
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.
23
24 import {DOM, REF} from '/library/js/Ref.js';
25
26 function isPointInElement(element, point)
27 {
28     if (!element || element.style.display == 'none')
29         return false;
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;
32 }
33
34 class _ToolTip {
35     constructor() {
36         this.ref = null;
37         this.arrow = null;
38         this.onArrowClick = null;
39
40         this.VERTICAL = 0;
41         this.HORIZONTAL = 1;
42     }
43     toString() {
44         const self = this;
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')
50                         return;
51                     if (!isPointInElement(self.arrow.element, event))
52                         this.unset()
53                 });
54             },
55             onStateUpdate: (element, stateDiff, state) => {
56                 if (stateDiff.content) {
57                     DOM.inject(element, stateDiff.content);
58                     element.style.display = null;
59                 } else {
60                     element.style.display = 'none';
61                     DOM.inject(element, '');
62                 }
63
64                 if (stateDiff.points) {
65                     element.style.left = '0px';
66                     element.style.top = '0px';
67
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);
74
75                     let direction = 'down';
76                     let point = upperPoint;
77
78                     if (upperPoint.y == lowerPoint.y) {
79                         // Horizontal tooltip
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];
82
83                         direction = 'left';
84                         let tipX = leftPoint.x - 12 - bounds.width;
85                         point = rightPoint;
86                         if (tipX < 0 || tipX + bounds.width + (rightPoint.x - leftPoint.x) / 2 < viewportWitdh / 2) {
87                             direction = 'right';
88                             tipX = rightPoint.x + 16;
89                             point = rightPoint;
90                         }
91                         element.style.left = `${tipX}px`;
92
93                         let tipY = point.y - bounds.height / 2;
94                         if (tipY + bounds.height > scrollDelta + viewportHeight)
95                             tipY = scrollDelta + viewportHeight - bounds.height;
96                         if (tipY < 0)
97                             tipY = 0;
98                         element.style.top = `${tipY}px`;
99                     } else {
100                         // Make an effort to place the tooltip in the center of the viewport.
101                         let tipY = upperPoint.y - 8 - bounds.height;
102                         point = upperPoint;
103                         if (tipY < scrollDelta || tipY + bounds.height + (lowerPoint.y - upperPoint.y) / 2 < scrollDelta + viewportHeight / 2) {
104                             direction = 'up';
105                             tipY = lowerPoint.y + 16;
106                             point = lowerPoint;
107                         }
108                         element.style.top = `${tipY}px`;
109
110                         let tipX = point.x - bounds.width / 2;
111                         if (tipX + bounds.width > viewportWitdh)
112                             tipX = viewportWitdh - bounds.width;
113                         if (tipX < 0)
114                             tipX = 0;
115                         element.style.left = `${tipX}px`;
116                     }
117
118                     self.arrow.setState({direction: direction, location: point});
119                 }
120             },
121         });
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')
127                         return;
128                     if (!isPointInElement(self.ref.element, event) && !isPointInElement(element, event))
129                         this.unset()
130                 });
131             },
132             onStateUpdate: (element, stateDiff) => {
133                 if (!stateDiff.direction || !stateDiff.location) {
134                     element.style.display = 'none';
135                     element.onclick = null;
136                     element.style.cursor = null;
137                     return;
138                 }
139
140                 if (self.onArrowClick) {
141                     element.onclick = self.onArrowClick;
142                     element.style.cursor = 'pointer';
143                 } else {
144                     element.onclick = null;
145                     element.style.cursor = null;
146                 }
147
148                 element.classList = [`tooltip arrow-${stateDiff.direction}`];
149                 
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`;
159                 } else {
160                     element.style.left = `${stateDiff.location.x - 15}px`;
161                     element.style.top = `${stateDiff.location.y - 13}px`;
162                 }
163                 element.style.display = null;
164             },
165
166         });
167         return `<div class="tooltip arrow-up" ref="${this.arrow}"></div>
168             <div class="tooltip-content" ref="${this.ref}">
169             </div>`;
170     }
171     set(content, points, onArrowClick = null) {
172         if (!this.ref) {
173             console.error('Cannot set ToolTip content, no tooltip on the page');
174             return;
175         }
176         if (!points || points.length == 0) {
177             console.error('Tool tips require a location');
178             return;
179         }
180         this.onArrowClick = onArrowClick;
181         this.ref.setState({content: content, points: points});
182     }
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;
187
188         // Manage the scroll delta
189         let scrollDelta = 0;
190         if (window.getComputedStyle(element.offsetParent).getPropertyValue('position') == 'fixed')
191             scrollDelta = document.documentElement.scrollTop || document.body.scrollTop;
192
193         if (options.orientation) {
194             this.set(content, [
195                 {x: bound.right, y: (bound.top + bound.bottom) / 2 + scrollDelta},
196                 {x: bound.left, y: (bound.top + bound.bottom) / 2 + scrollDelta},
197             ], onArrowClick);
198         } else {
199             this.set(content, [
200                 {x: (bound.right + bound.left) / 2, y: bound.top + scrollDelta},
201                 {x: (bound.right + bound.left) / 2, y: bound.bottom + scrollDelta},
202             ], onArrowClick);
203         }
204     }
205     unset() {
206         if (this.ref)
207             this.ref.setState({content: null, points: null});
208         if (this.arrow)
209             this.arrow.setState({direction: null, points: null});
210     }
211     isIn(point) {
212         return isPointInElement(this.ref.element, point) || isPointInElement(this.arrow.element, point);
213     }
214 }
215
216 const ToolTip = new _ToolTip();
217
218 export {ToolTip};