Home | History | Annotate | Download | only in front-end
      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 WebInspector.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 
     46 WebInspector.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 
    157 WebInspector.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 
    169 WebInspector.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