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