• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
31/**
32 * @constructor
33 * @extends {WebInspector.View}
34 * @param {!WebInspector.PopoverHelper=} popoverHelper
35 */
36WebInspector.Popover = function(popoverHelper)
37{
38    WebInspector.View.call(this);
39    this.markAsRoot();
40    this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll"; // Override
41
42    this._popupArrowElement = document.createElement("div");
43    this._popupArrowElement.className = "arrow";
44    this.element.appendChild(this._popupArrowElement);
45
46    this._contentDiv = document.createElement("div");
47    this._contentDiv.className = "content";
48    this.element.appendChild(this._contentDiv);
49
50    this._popoverHelper = popoverHelper;
51}
52
53WebInspector.Popover.prototype = {
54    /**
55     * @param {!Element} element
56     * @param {!Element|!AnchorBox} anchor
57     * @param {?number=} preferredWidth
58     * @param {?number=} preferredHeight
59     * @param {?WebInspector.Popover.Orientation=} arrowDirection
60     */
61    show: function(element, anchor, preferredWidth, preferredHeight, arrowDirection)
62    {
63        this._innerShow(null, element, anchor, preferredWidth, preferredHeight, arrowDirection);
64    },
65
66    /**
67     * @param {!WebInspector.View} view
68     * @param {!Element|!AnchorBox} anchor
69     * @param {?number=} preferredWidth
70     * @param {?number=} preferredHeight
71     */
72    showView: function(view, anchor, preferredWidth, preferredHeight)
73    {
74        this._innerShow(view, view.element, anchor, preferredWidth, preferredHeight);
75    },
76
77    /**
78     * @param {?WebInspector.View} view
79     * @param {!Element} contentElement
80     * @param {!Element|!AnchorBox} anchor
81     * @param {?number=} preferredWidth
82     * @param {?number=} preferredHeight
83     * @param {?WebInspector.Popover.Orientation=} arrowDirection
84     */
85    _innerShow: function(view, contentElement, anchor, preferredWidth, preferredHeight, arrowDirection)
86    {
87        if (this._disposed)
88            return;
89        this.contentElement = contentElement;
90
91        // This should not happen, but we hide previous popup to be on the safe side.
92        if (WebInspector.Popover._popover)
93            WebInspector.Popover._popover.detach();
94        WebInspector.Popover._popover = this;
95
96        // Temporarily attach in order to measure preferred dimensions.
97        var preferredSize = view ? view.measurePreferredSize() : this.contentElement.measurePreferredSize();
98        preferredWidth = preferredWidth || preferredSize.width;
99        preferredHeight = preferredHeight || preferredSize.height;
100
101        WebInspector.View.prototype.show.call(this, document.body);
102
103        if (view)
104            view.show(this._contentDiv);
105        else
106            this._contentDiv.appendChild(this.contentElement);
107
108        this._positionElement(anchor, preferredWidth, preferredHeight, arrowDirection);
109
110        if (this._popoverHelper) {
111            this._contentDiv.addEventListener("mousemove", this._popoverHelper._killHidePopoverTimer.bind(this._popoverHelper), true);
112            this.element.addEventListener("mouseout", this._popoverHelper._popoverMouseOut.bind(this._popoverHelper), true);
113        }
114    },
115
116    hide: function()
117    {
118        this.detach();
119        delete WebInspector.Popover._popover;
120    },
121
122    get disposed()
123    {
124        return this._disposed;
125    },
126
127    dispose: function()
128    {
129        if (this.isShowing())
130            this.hide();
131        this._disposed = true;
132    },
133
134    setCanShrink: function(canShrink)
135    {
136        this._hasFixedHeight = !canShrink;
137        this._contentDiv.classList.add("fixed-height");
138    },
139
140    /**
141     * @param {!Element|!AnchorBox} anchorElement
142     * @param {number} preferredWidth
143     * @param {number} preferredHeight
144     * @param {?WebInspector.Popover.Orientation=} arrowDirection
145     */
146    _positionElement: function(anchorElement, preferredWidth, preferredHeight, arrowDirection)
147    {
148        const borderWidth = 25;
149        const scrollerWidth = this._hasFixedHeight ? 0 : 11;
150        const arrowHeight = 15;
151        const arrowOffset = 10;
152        const borderRadius = 10;
153
154        // Skinny tooltips are not pretty, their arrow location is not nice.
155        preferredWidth = Math.max(preferredWidth, 50);
156        // Position relative to main DevTools element.
157        const container = WebInspector.Dialog.modalHostView().element;
158        const totalWidth = container.offsetWidth;
159        const totalHeight = container.offsetHeight;
160
161        var anchorBox = anchorElement instanceof AnchorBox ? anchorElement : anchorElement.boxInWindow(window);
162        anchorBox = anchorBox.relativeToElement(container);
163        var newElementPosition = { x: 0, y: 0, width: preferredWidth + scrollerWidth, height: preferredHeight };
164
165        var verticalAlignment;
166        var roomAbove = anchorBox.y;
167        var roomBelow = totalHeight - anchorBox.y - anchorBox.height;
168
169        if ((roomAbove > roomBelow) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) {
170            // Positioning above the anchor.
171            if ((anchorBox.y > newElementPosition.height + arrowHeight + borderRadius) || (arrowDirection === WebInspector.Popover.Orientation.Bottom))
172                newElementPosition.y = anchorBox.y - newElementPosition.height - arrowHeight;
173            else {
174                newElementPosition.y = borderRadius;
175                newElementPosition.height = anchorBox.y - borderRadius * 2 - arrowHeight;
176                if (this._hasFixedHeight && newElementPosition.height < preferredHeight) {
177                    newElementPosition.y = borderRadius;
178                    newElementPosition.height = preferredHeight;
179                }
180            }
181            verticalAlignment = WebInspector.Popover.Orientation.Bottom;
182        } else {
183            // Positioning below the anchor.
184            newElementPosition.y = anchorBox.y + anchorBox.height + arrowHeight;
185            if ((newElementPosition.y + newElementPosition.height + borderRadius >= totalHeight) && (arrowDirection !== WebInspector.Popover.Orientation.Top)) {
186                newElementPosition.height = totalHeight - borderRadius - newElementPosition.y;
187                if (this._hasFixedHeight && newElementPosition.height < preferredHeight) {
188                    newElementPosition.y = totalHeight - preferredHeight - borderRadius;
189                    newElementPosition.height = preferredHeight;
190                }
191            }
192            // Align arrow.
193            verticalAlignment = WebInspector.Popover.Orientation.Top;
194        }
195
196        var horizontalAlignment;
197        if (anchorBox.x + newElementPosition.width < totalWidth) {
198            newElementPosition.x = Math.max(borderRadius, anchorBox.x - borderRadius - arrowOffset);
199            horizontalAlignment = "left";
200        } else if (newElementPosition.width + borderRadius * 2 < totalWidth) {
201            newElementPosition.x = totalWidth - newElementPosition.width - borderRadius;
202            horizontalAlignment = "right";
203            // Position arrow accurately.
204            var arrowRightPosition = Math.max(0, totalWidth - anchorBox.x - anchorBox.width - borderRadius - arrowOffset);
205            arrowRightPosition += anchorBox.width / 2;
206            arrowRightPosition = Math.min(arrowRightPosition, newElementPosition.width - borderRadius - arrowOffset);
207            this._popupArrowElement.style.right = arrowRightPosition + "px";
208        } else {
209            newElementPosition.x = borderRadius;
210            newElementPosition.width = totalWidth - borderRadius * 2;
211            newElementPosition.height += scrollerWidth;
212            horizontalAlignment = "left";
213            if (verticalAlignment === WebInspector.Popover.Orientation.Bottom)
214                newElementPosition.y -= scrollerWidth;
215            // Position arrow accurately.
216            this._popupArrowElement.style.left = Math.max(0, anchorBox.x - borderRadius * 2 - arrowOffset) + "px";
217            this._popupArrowElement.style.left += anchorBox.width / 2;
218        }
219
220        this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll " + verticalAlignment + "-" + horizontalAlignment + "-arrow";
221        this.element.positionAt(newElementPosition.x - borderWidth, newElementPosition.y - borderWidth, container);
222        this.element.style.width = newElementPosition.width + borderWidth * 2 + "px";
223        this.element.style.height = newElementPosition.height + borderWidth * 2 + "px";
224    },
225
226    __proto__: WebInspector.View.prototype
227}
228
229/**
230 * @constructor
231 * @param {!Element} panelElement
232 * @param {function(!Element, !Event):(!Element|!AnchorBox)|undefined} getAnchor
233 * @param {function(!Element, !WebInspector.Popover):undefined} showPopover
234 * @param {function()=} onHide
235 * @param {boolean=} disableOnClick
236 */
237WebInspector.PopoverHelper = function(panelElement, getAnchor, showPopover, onHide, disableOnClick)
238{
239    this._panelElement = panelElement;
240    this._getAnchor = getAnchor;
241    this._showPopover = showPopover;
242    this._onHide = onHide;
243    this._disableOnClick = !!disableOnClick;
244    panelElement.addEventListener("mousedown", this._mouseDown.bind(this), false);
245    panelElement.addEventListener("mousemove", this._mouseMove.bind(this), false);
246    panelElement.addEventListener("mouseout", this._mouseOut.bind(this), false);
247    this.setTimeout(1000);
248}
249
250WebInspector.PopoverHelper.prototype = {
251    setTimeout: function(timeout)
252    {
253        this._timeout = timeout;
254    },
255
256    /**
257     * @param {!MouseEvent} event
258     * @return {boolean}
259     */
260    _eventInHoverElement: function(event)
261    {
262        if (!this._hoverElement)
263            return false;
264        var box = this._hoverElement instanceof AnchorBox ? this._hoverElement : this._hoverElement.boxInWindow();
265        return (box.x <= event.clientX && event.clientX <= box.x + box.width &&
266            box.y <= event.clientY && event.clientY <= box.y + box.height);
267    },
268
269    _mouseDown: function(event)
270    {
271        if (this._disableOnClick || !this._eventInHoverElement(event))
272            this.hidePopover();
273        else {
274            this._killHidePopoverTimer();
275            this._handleMouseAction(event, true);
276        }
277    },
278
279    _mouseMove: function(event)
280    {
281        // Pretend that nothing has happened.
282        if (this._eventInHoverElement(event))
283            return;
284
285        this._startHidePopoverTimer();
286        this._handleMouseAction(event, false);
287    },
288
289    _popoverMouseOut: function(event)
290    {
291        if (!this.isPopoverVisible())
292            return;
293        if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(this._popover._contentDiv))
294            this._startHidePopoverTimer();
295    },
296
297    _mouseOut: function(event)
298    {
299        if (!this.isPopoverVisible())
300            return;
301        if (!this._eventInHoverElement(event))
302            this._startHidePopoverTimer();
303    },
304
305    _startHidePopoverTimer: function()
306    {
307        // User has 500ms (this._timeout / 2) to reach the popup.
308        if (!this._popover || this._hidePopoverTimer)
309            return;
310
311        /**
312         * @this {WebInspector.PopoverHelper}
313         */
314        function doHide()
315        {
316            this._hidePopover();
317            delete this._hidePopoverTimer;
318        }
319        this._hidePopoverTimer = setTimeout(doHide.bind(this), this._timeout / 2);
320    },
321
322    _handleMouseAction: function(event, isMouseDown)
323    {
324        this._resetHoverTimer();
325        if (event.which && this._disableOnClick)
326            return;
327        this._hoverElement = this._getAnchor(event.target, event);
328        if (!this._hoverElement)
329            return;
330        const toolTipDelay = isMouseDown ? 0 : (this._popup ? this._timeout * 0.6 : this._timeout);
331        this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay);
332    },
333
334    _resetHoverTimer: function()
335    {
336        if (this._hoverTimer) {
337            clearTimeout(this._hoverTimer);
338            delete this._hoverTimer;
339        }
340    },
341
342    /**
343     * @return {boolean}
344     */
345    isPopoverVisible: function()
346    {
347        return !!this._popover;
348    },
349
350    hidePopover: function()
351    {
352        this._resetHoverTimer();
353        this._hidePopover();
354    },
355
356    _hidePopover: function()
357    {
358        if (!this._popover)
359            return;
360
361        if (this._onHide)
362            this._onHide();
363
364        this._popover.dispose();
365        delete this._popover;
366        this._hoverElement = null;
367    },
368
369    _mouseHover: function(element)
370    {
371        delete this._hoverTimer;
372
373        this._hidePopover();
374        this._popover = new WebInspector.Popover(this);
375        this._showPopover(element, this._popover);
376    },
377
378    _killHidePopoverTimer: function()
379    {
380        if (this._hidePopoverTimer) {
381            clearTimeout(this._hidePopoverTimer);
382            delete this._hidePopoverTimer;
383
384            // We know that we reached the popup, but we might have moved over other elements.
385            // Discard pending command.
386            this._resetHoverTimer();
387        }
388    }
389}
390
391/** @enum {string} */
392WebInspector.Popover.Orientation = {
393    Top: "top",
394    Bottom: "bottom"
395}
396