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 /**
     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";
     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 
     53 WebInspector.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             contentElement.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.addStyleClass("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         const totalWidth = window.innerWidth;
    157         const totalHeight = window.innerHeight;
    158 
    159         var anchorBox = anchorElement instanceof AnchorBox ? anchorElement : anchorElement.boxInWindow(window);
    160         var newElementPosition = { x: 0, y: 0, width: preferredWidth + scrollerWidth, height: preferredHeight };
    161 
    162         var verticalAlignment;
    163         var roomAbove = anchorBox.y;
    164         var roomBelow = totalHeight - anchorBox.y - anchorBox.height;
    165 
    166         if ((roomAbove > roomBelow) || (arrowDirection === WebInspector.Popover.Orientation.Bottom)) {
    167             // Positioning above the anchor.
    168             if ((anchorBox.y > newElementPosition.height + arrowHeight + borderRadius) || (arrowDirection === WebInspector.Popover.Orientation.Bottom))
    169                 newElementPosition.y = anchorBox.y - newElementPosition.height - arrowHeight;
    170             else {
    171                 newElementPosition.y = borderRadius;
    172                 newElementPosition.height = anchorBox.y - borderRadius * 2 - arrowHeight;
    173                 if (this._hasFixedHeight && newElementPosition.height < preferredHeight) {
    174                     newElementPosition.y = borderRadius;
    175                     newElementPosition.height = preferredHeight;
    176                 }
    177             }
    178             verticalAlignment = WebInspector.Popover.Orientation.Bottom;
    179         } else {
    180             // Positioning below the anchor.
    181             newElementPosition.y = anchorBox.y + anchorBox.height + arrowHeight;
    182             if ((newElementPosition.y + newElementPosition.height + arrowHeight - borderWidth >= totalHeight) && (arrowDirection !== WebInspector.Popover.Orientation.Top)) {
    183                 newElementPosition.height = totalHeight - anchorBox.y - anchorBox.height - borderRadius * 2 - arrowHeight;
    184                 if (this._hasFixedHeight && newElementPosition.height < preferredHeight) {
    185                     newElementPosition.y = totalHeight - preferredHeight - borderRadius;
    186                     newElementPosition.height = preferredHeight;
    187                 }
    188             }
    189             // Align arrow.
    190             verticalAlignment = WebInspector.Popover.Orientation.Top;
    191         }
    192 
    193         var horizontalAlignment;
    194         if (anchorBox.x + newElementPosition.width < totalWidth) {
    195             newElementPosition.x = Math.max(borderRadius, anchorBox.x - borderRadius - arrowOffset);
    196             horizontalAlignment = "left";
    197         } else if (newElementPosition.width + borderRadius * 2 < totalWidth) {
    198             newElementPosition.x = totalWidth - newElementPosition.width - borderRadius;
    199             horizontalAlignment = "right";
    200             // Position arrow accurately.
    201             var arrowRightPosition = Math.max(0, totalWidth - anchorBox.x - anchorBox.width - borderRadius - arrowOffset);
    202             arrowRightPosition += anchorBox.width / 2;
    203             arrowRightPosition = Math.min(arrowRightPosition, newElementPosition.width - borderRadius - arrowOffset);
    204             this._popupArrowElement.style.right = arrowRightPosition + "px";
    205         } else {
    206             newElementPosition.x = borderRadius;
    207             newElementPosition.width = totalWidth - borderRadius * 2;
    208             newElementPosition.height += scrollerWidth;
    209             horizontalAlignment = "left";
    210             if (verticalAlignment === WebInspector.Popover.Orientation.Bottom)
    211                 newElementPosition.y -= scrollerWidth;
    212             // Position arrow accurately.
    213             this._popupArrowElement.style.left = Math.max(0, anchorBox.x - borderRadius * 2 - arrowOffset) + "px";
    214             this._popupArrowElement.style.left += anchorBox.width / 2;
    215         }
    216 
    217         this.element.className = "popover custom-popup-vertical-scroll custom-popup-horizontal-scroll " + verticalAlignment + "-" + horizontalAlignment + "-arrow";
    218         this.element.positionAt(newElementPosition.x - borderWidth, newElementPosition.y - borderWidth);
    219         this.element.style.width = newElementPosition.width + borderWidth * 2 + "px";
    220         this.element.style.height = newElementPosition.height + borderWidth * 2 + "px";
    221     },
    222 
    223     __proto__: WebInspector.View.prototype
    224 }
    225 
    226 /**
    227  * @constructor
    228  * @param {Element} panelElement
    229  * @param {function(Element, Event):(Element|AnchorBox)|undefined} getAnchor
    230  * @param {function(Element, WebInspector.Popover):undefined} showPopover
    231  * @param {function()=} onHide
    232  * @param {boolean=} disableOnClick
    233  */
    234 WebInspector.PopoverHelper = function(panelElement, getAnchor, showPopover, onHide, disableOnClick)
    235 {
    236     this._panelElement = panelElement;
    237     this._getAnchor = getAnchor;
    238     this._showPopover = showPopover;
    239     this._onHide = onHide;
    240     this._disableOnClick = !!disableOnClick;
    241     panelElement.addEventListener("mousedown", this._mouseDown.bind(this), false);
    242     panelElement.addEventListener("mousemove", this._mouseMove.bind(this), false);
    243     panelElement.addEventListener("mouseout", this._mouseOut.bind(this), false);
    244     this.setTimeout(1000);
    245 }
    246 
    247 WebInspector.PopoverHelper.prototype = {
    248     setTimeout: function(timeout)
    249     {
    250         this._timeout = timeout;
    251     },
    252 
    253     /**
    254      * @param {MouseEvent} event
    255      * @return {boolean}
    256      */
    257     _eventInHoverElement: function(event)
    258     {
    259         if (!this._hoverElement)
    260             return false;
    261         var box = this._hoverElement instanceof AnchorBox ? this._hoverElement : this._hoverElement.boxInWindow();
    262         return (box.x <= event.clientX && event.clientX <= box.x + box.width &&
    263             box.y <= event.clientY && event.clientY <= box.y + box.height);
    264     },
    265 
    266     _mouseDown: function(event)
    267     {
    268         if (this._disableOnClick || !this._eventInHoverElement(event))
    269             this.hidePopover();
    270         else {
    271             this._killHidePopoverTimer();
    272             this._handleMouseAction(event, true);
    273         }
    274     },
    275 
    276     _mouseMove: function(event)
    277     {
    278         // Pretend that nothing has happened.
    279         if (this._eventInHoverElement(event))
    280             return;
    281 
    282         this._startHidePopoverTimer();
    283         this._handleMouseAction(event, false);
    284     },
    285 
    286     _popoverMouseOut: function(event)
    287     {
    288         if (!this.isPopoverVisible())
    289             return;
    290         if (event.relatedTarget && !event.relatedTarget.isSelfOrDescendant(this._popover._contentDiv))
    291             this._startHidePopoverTimer();
    292     },
    293 
    294     _mouseOut: function(event)
    295     {
    296         if (!this.isPopoverVisible())
    297             return;
    298         if (!this._eventInHoverElement(event))
    299             this._startHidePopoverTimer();
    300     },
    301 
    302     _startHidePopoverTimer: function()
    303     {
    304         // User has 500ms (this._timeout / 2) to reach the popup.
    305         if (!this._popover || this._hidePopoverTimer)
    306             return;
    307 
    308         function doHide()
    309         {
    310             this._hidePopover();
    311             delete this._hidePopoverTimer;
    312         }
    313         this._hidePopoverTimer = setTimeout(doHide.bind(this), this._timeout / 2);
    314     },
    315 
    316     _handleMouseAction: function(event, isMouseDown)
    317     {
    318         this._resetHoverTimer();
    319         if (event.which && this._disableOnClick)
    320             return;
    321         this._hoverElement = this._getAnchor(event.target, event);
    322         if (!this._hoverElement)
    323             return;
    324         const toolTipDelay = isMouseDown ? 0 : (this._popup ? this._timeout * 0.6 : this._timeout);
    325         this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay);
    326     },
    327 
    328     _resetHoverTimer: function()
    329     {
    330         if (this._hoverTimer) {
    331             clearTimeout(this._hoverTimer);
    332             delete this._hoverTimer;
    333         }
    334     },
    335 
    336     isPopoverVisible: function()
    337     {
    338         return !!this._popover;
    339     },
    340 
    341     hidePopover: function()
    342     {
    343         this._resetHoverTimer();
    344         this._hidePopover();
    345     },
    346 
    347     _hidePopover: function()
    348     {
    349         if (!this._popover)
    350             return;
    351 
    352         if (this._onHide)
    353             this._onHide();
    354 
    355         this._popover.dispose();
    356         delete this._popover;
    357         this._hoverElement = null;
    358     },
    359 
    360     _mouseHover: function(element)
    361     {
    362         delete this._hoverTimer;
    363 
    364         this._hidePopover();
    365         this._popover = new WebInspector.Popover(this);
    366         this._showPopover(element, this._popover);
    367     },
    368 
    369     _killHidePopoverTimer: function()
    370     {
    371         if (this._hidePopoverTimer) {
    372             clearTimeout(this._hidePopoverTimer);
    373             delete this._hidePopoverTimer;
    374 
    375             // We know that we reached the popup, but we might have moved over other elements.
    376             // Discard pending command.
    377             this._resetHoverTimer();
    378         }
    379     }
    380 }
    381 
    382 /** @enum {string} */
    383 WebInspector.Popover.Orientation = {
    384     Top: "top",
    385     Bottom: "bottom"
    386 }
    387 
    388 /**
    389  * @constructor
    390  * @param {string} title
    391  */
    392 WebInspector.PopoverContentHelper = function(title)
    393 {
    394     this._contentTable = document.createElement("table");
    395     var titleCell = this._createCell(WebInspector.UIString("%s - Details", title), "popover-details-title");
    396     titleCell.colSpan = 2;
    397     var titleRow = document.createElement("tr");
    398     titleRow.appendChild(titleCell);
    399     this._contentTable.appendChild(titleRow);
    400 }
    401 
    402 WebInspector.PopoverContentHelper.prototype = {
    403     contentTable: function()
    404     {
    405         return this._contentTable;
    406     },
    407 
    408     /**
    409      * @param {string=} styleName
    410      */
    411     _createCell: function(content, styleName)
    412     {
    413         var text = document.createElement("label");
    414         text.appendChild(document.createTextNode(content));
    415         var cell = document.createElement("td");
    416         cell.className = "popover-details";
    417         if (styleName)
    418             cell.className += " " + styleName;
    419         cell.textContent = content;
    420         return cell;
    421     },
    422 
    423     appendTextRow: function(title, content)
    424     {
    425         var row = document.createElement("tr");
    426         row.appendChild(this._createCell(title, "popover-details-row-title"));
    427         row.appendChild(this._createCell(content, "popover-details-row-data"));
    428         this._contentTable.appendChild(row);
    429     },
    430 
    431     /**
    432      * @param {string=} titleStyle
    433      */
    434     appendElementRow: function(title, content, titleStyle)
    435     {
    436         var row = document.createElement("tr");
    437         var titleCell = this._createCell(title, "popover-details-row-title");
    438         if (titleStyle)
    439             titleCell.addStyleClass(titleStyle);
    440         row.appendChild(titleCell);
    441         var cell = document.createElement("td");
    442         cell.className = "details";
    443         cell.appendChild(content);
    444         row.appendChild(cell);
    445         this._contentTable.appendChild(row);
    446     },
    447 
    448     appendStackTrace: function(title, stackTrace, callFrameLinkifier)
    449     {
    450         this.appendTextRow("", "");
    451         var framesTable = document.createElement("table");
    452         for (var i = 0; i < stackTrace.length; ++i) {
    453             var stackFrame = stackTrace[i];
    454             var row = document.createElement("tr");
    455             row.className = "details";
    456             row.appendChild(this._createCell(stackFrame.functionName ? stackFrame.functionName : WebInspector.UIString("(anonymous function)"), "function-name"));
    457             row.appendChild(this._createCell(" @ "));
    458             var linkCell = document.createElement("td");
    459             var urlElement = callFrameLinkifier(stackFrame);
    460             linkCell.appendChild(urlElement);
    461             row.appendChild(linkCell);
    462             framesTable.appendChild(row);
    463         }
    464         this.appendElementRow(title, framesTable, "popover-stacktrace-title");
    465     }
    466 }
    467