1/*
2 * Copyright (C) 2009 Google Inc. All rights reserved.
3 *
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions are
6 * met:
7 *
8 *     * Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 *     * Redistributions in binary form must reproduce the above
11 * copyright notice, this list of conditions and the following disclaimer
12 * in the documentation and/or other materials provided with the
13 * distribution.
14 *     * Neither the name of Google Inc. nor the names of its
15 * contributors may be used to endorse or promote products derived from
16 * this software without specific prior written permission.
17 *
18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 */
30
31WebInspector.Popover = function(contentElement)
32{
33    this.element = document.createElement("div");
34    this.element.className = "popover";
35
36    this._popupArrowElement = document.createElement("div");
37    this._popupArrowElement.className = "arrow";
38    this.element.appendChild(this._popupArrowElement);
39
40    this.contentElement = contentElement;
41    this._contentDiv = document.createElement("div");
42    this._contentDiv.className = "content";
43    this._visible = false;
44}
45
46WebInspector.Popover.prototype = {
47    show: function(anchor, preferredWidth, preferredHeight)
48    {
49        // This should not happen, but we hide previous popup to be on the safe side.
50        if (WebInspector.Popover._popoverElement)
51            document.body.removeChild(WebInspector.Popover._popoverElement);
52        WebInspector.Popover._popoverElement = this.element;
53
54        // Temporarily attach in order to measure preferred dimensions.
55        this.contentElement.positionAt(0, 0);
56        document.body.appendChild(this.contentElement);
57        var preferredWidth = preferredWidth || this.contentElement.offsetWidth;
58        var preferredHeight = preferredHeight || this.contentElement.offsetHeight;
59
60        this._contentDiv.appendChild(this.contentElement);
61        this.element.appendChild(this._contentDiv);
62        document.body.appendChild(this.element);
63        this._positionElement(anchor, preferredWidth, preferredHeight);
64        this._visible = true;
65    },
66
67    hide: function()
68    {
69        if (WebInspector.Popover._popoverElement) {
70            delete WebInspector.Popover._popoverElement;
71            document.body.removeChild(this.element);
72        }
73        this._visible = false;
74    },
75
76    get visible()
77    {
78        return this._visible;
79    },
80
81    _positionElement: function(anchorElement, preferredWidth, preferredHeight)
82    {
83        const borderWidth = 25;
84        const scrollerWidth = 11;
85        const arrowHeight = 15;
86        const arrowOffset = 10;
87        const borderRadius = 10;
88
89        // Skinny tooltips are not pretty, their arrow location is not nice.
90        preferredWidth = Math.max(preferredWidth, 50);
91        const totalWidth = window.innerWidth;
92        const totalHeight = window.innerHeight;
93
94        var anchorBox = {x: anchorElement.totalOffsetLeft, y: anchorElement.totalOffsetTop, width: anchorElement.offsetWidth, height: anchorElement.offsetHeight};
95        while (anchorElement !== document.body) {
96            if (anchorElement.scrollLeft)
97                anchorBox.x -= anchorElement.scrollLeft;
98            if (anchorElement.scrollTop)
99                anchorBox.y -= anchorElement.scrollTop;
100            anchorElement = anchorElement.parentElement;
101        }
102
103        var newElementPosition = { x: 0, y: 0, width: preferredWidth + scrollerWidth, height: preferredHeight };
104
105        var verticalAlignment;
106        var roomAbove = anchorBox.y;
107        var roomBelow = totalHeight - anchorBox.y - anchorBox.height;
108
109        if (roomAbove > roomBelow) {
110            // Positioning above the anchor.
111            if (anchorBox.y > newElementPosition.height + arrowHeight + borderRadius)
112                newElementPosition.y = anchorBox.y - newElementPosition.height - arrowHeight;
113            else {
114                newElementPosition.y = borderRadius * 2;
115                newElementPosition.height = anchorBox.y - borderRadius * 2 - arrowHeight;
116            }
117            verticalAlignment = "bottom";
118        } else {
119            // Positioning below the anchor.
120            newElementPosition.y = anchorBox.y + anchorBox.height + arrowHeight;
121            if (newElementPosition.y + newElementPosition.height + arrowHeight - borderWidth >= totalHeight)
122                newElementPosition.height = totalHeight - anchorBox.y - anchorBox.height - borderRadius * 2 - arrowHeight;
123            // Align arrow.
124            verticalAlignment = "top";
125        }
126
127        var horizontalAlignment;
128        if (anchorBox.x + newElementPosition.width < totalWidth) {
129            newElementPosition.x = Math.max(borderRadius, anchorBox.x - borderRadius - arrowOffset);
130            horizontalAlignment = "left";
131        } else if (newElementPosition.width + borderRadius * 2 < totalWidth) {
132            newElementPosition.x = totalWidth - newElementPosition.width - borderRadius;
133            horizontalAlignment = "right";
134            // Position arrow accurately.
135            var arrowRightPosition = Math.max(0, totalWidth - anchorBox.x - anchorBox.width - borderRadius - arrowOffset);
136            arrowRightPosition += anchorBox.width / 2;
137            this._popupArrowElement.style.right = arrowRightPosition + "px";
138        } else {
139            newElementPosition.x = borderRadius;
140            newElementPosition.width = totalWidth - borderRadius * 2;
141            newElementPosition.height += scrollerWidth;
142            horizontalAlignment = "left";
143            if (verticalAlignment === "bottom")
144                newElementPosition.y -= scrollerWidth;
145            // Position arrow accurately.
146            this._popupArrowElement.style.left = Math.max(0, anchorBox.x - borderRadius * 2 - arrowOffset) + "px";
147            this._popupArrowElement.style.left += anchorBox.width / 2;
148        }
149
150        this.element.className = "popover " + verticalAlignment + "-" + horizontalAlignment + "-arrow";
151        this.element.positionAt(newElementPosition.x - borderWidth, newElementPosition.y - borderWidth);
152        this.element.style.width = newElementPosition.width + borderWidth * 2 + "px";
153        this.element.style.height = newElementPosition.height + borderWidth * 2 + "px";
154    }
155}
156
157WebInspector.PopoverHelper = function(panelElement, getAnchor, showPopup, showOnClick, onHide)
158{
159    this._panelElement = panelElement;
160    this._getAnchor = getAnchor;
161    this._showPopup = showPopup;
162    this._showOnClick = showOnClick;
163    this._onHide = onHide;
164    panelElement.addEventListener("mousedown", this._mouseDown.bind(this), false);
165    panelElement.addEventListener("mousemove", this._mouseMove.bind(this), false);
166    this.setTimeout(1000);
167}
168
169WebInspector.PopoverHelper.prototype = {
170    setTimeout: function(timeout)
171    {
172         this._timeout = timeout;
173    },
174
175    _mouseDown: function(event)
176    {
177        this._killHidePopupTimer();
178        this._handleMouseAction(event, true);
179    },
180
181    _mouseMove: function(event)
182    {
183        // Pretend that nothing has happened.
184        if (this._hoverElement === event.target || (this._hoverElement && this._hoverElement.isAncestor(event.target)))
185            return;
186
187        // User has 500ms (this._timeout / 2) to reach the popup.
188        if (this._popup && !this._hidePopupTimer) {
189            var self = this;
190            function doHide()
191            {
192                self._hidePopup();
193                delete self._hidePopupTimer;
194            }
195            this._hidePopupTimer = setTimeout(doHide, this._timeout / 2);
196        }
197
198        this._handleMouseAction(event);
199    },
200
201    _handleMouseAction: function(event, isMouseDown)
202    {
203        this._resetHoverTimer();
204
205        this._hoverElement = this._getAnchor(event.target);
206        if (!this._hoverElement)
207            return;
208
209        const toolTipDelay = isMouseDown ? 0 : (this._popup ? this._timeout * 0.6 : this._timeout);
210        this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay);
211    },
212
213    _resetHoverTimer: function()
214    {
215        if (this._hoverTimer) {
216            clearTimeout(this._hoverTimer);
217            delete this._hoverTimer;
218        }
219    },
220
221    hidePopup: function()
222    {
223        this._resetHoverTimer();
224        this._hidePopup();
225    },
226
227    _hidePopup: function()
228    {
229        if (!this._popup)
230            return;
231
232        if (this._onHide)
233            this._onHide();
234
235        this._popup.hide();
236        delete this._popup;
237    },
238
239    _mouseHover: function(element)
240    {
241        delete this._hoverTimer;
242
243        this._popup = this._showPopup(element);
244        if (this._popup)
245            this._popup.contentElement.addEventListener("mousemove", this._killHidePopupTimer.bind(this), true);
246    },
247
248    _killHidePopupTimer: function()
249    {
250        if (this._hidePopupTimer) {
251            clearTimeout(this._hidePopupTimer);
252            delete this._hidePopupTimer;
253
254            // We know that we reached the popup, but we might have moved over other elements.
255            // Discard pending command.
256            this._resetHoverTimer();
257        }
258    }
259}
260