Popover.js   [plain text]


/*
 * Copyright (C) 2009 Google Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 * notice, this list of conditions and the following disclaimer.
 *     * Redistributions in binary form must reproduce the above
 * copyright notice, this list of conditions and the following disclaimer
 * in the documentation and/or other materials provided with the
 * distribution.
 *     * Neither the name of Google Inc. nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

WebInspector.Popover = function(contentElement)
{
    this.element = document.createElement("div");
    this.element.className = "popover";

    this._popupArrowElement = document.createElement("div");
    this._popupArrowElement.className = "arrow";
    this.element.appendChild(this._popupArrowElement);

    this.contentElement = contentElement;
    this._contentDiv = document.createElement("div");
    this._contentDiv.className = "content";
}

WebInspector.Popover.prototype = {
    show: function(anchor, preferredWidth, preferredHeight)
    {
        // This should not happen, but we hide previous popup to be on the safe side.
        if (WebInspector.Popover._popoverElement)
            document.body.removeChild(WebInspector.Popover._popoverElement);
        WebInspector.Popover._popoverElement = this.element;

        // Temporarily attach in order to measure preferred dimensions.
        this.contentElement.positionAt(0, 0);
        document.body.appendChild(this.contentElement);
        var preferredWidth = preferredWidth || this.contentElement.offsetWidth;
        var preferredHeight = preferredHeight || this.contentElement.offsetHeight;

        this._contentDiv.appendChild(this.contentElement);
        this.element.appendChild(this._contentDiv);
        document.body.appendChild(this.element);
        this._positionElement(anchor, preferredWidth, preferredHeight);
    },

    hide: function()
    {
        if (WebInspector.Popover._popoverElement) {
            delete WebInspector.Popover._popoverElement;
            document.body.removeChild(this.element);
        }
    },

    _positionElement: function(anchorElement, preferredWidth, preferredHeight)
    {
        const borderWidth = 25;
        const scrollerWidth = 11;
        const arrowHeight = 15;
        const arrowOffset = 10;
        const borderRadius = 10;

        // Skinny tooltips are not pretty, their arrow location is not nice.
        preferredWidth = Math.max(preferredWidth, 50);
        const totalWidth = window.innerWidth;
        const totalHeight = window.innerHeight;

        var anchorBox = {x: anchorElement.totalOffsetLeft, y: anchorElement.totalOffsetTop, width: anchorElement.offsetWidth, height: anchorElement.offsetHeight};
        while (anchorElement !== document.body) {
            if (anchorElement.scrollLeft)
                anchorBox.x -= anchorElement.scrollLeft;
            if (anchorElement.scrollTop)
                anchorBox.y -= anchorElement.scrollTop;
            anchorElement = anchorElement.parentElement;
        }

        var newElementPosition = { x: 0, y: 0, width: preferredWidth + scrollerWidth, height: preferredHeight };

        var verticalAlignment;
        var roomAbove = anchorBox.y;
        var roomBelow = totalHeight - anchorBox.y - anchorBox.height;

        if (roomAbove > roomBelow) {
            // Positioning above the anchor.
            if (anchorBox.y > newElementPosition.height + arrowHeight + borderRadius)
                newElementPosition.y = anchorBox.y - newElementPosition.height - arrowHeight;
            else {
                newElementPosition.y = borderRadius * 2;
                newElementPosition.height = anchorBox.y - borderRadius * 2 - arrowHeight;
            }
            verticalAlignment = "bottom";
        } else {
            // Positioning below the anchor.
            newElementPosition.y = anchorBox.y + anchorBox.height + arrowHeight;
            if (newElementPosition.y + newElementPosition.height + arrowHeight - borderWidth >= totalHeight)
                newElementPosition.height = totalHeight - anchorBox.y - anchorBox.height - borderRadius * 2 - arrowHeight;
            // Align arrow.
            verticalAlignment = "top";
        }

        var horizontalAlignment;
        if (anchorBox.x + newElementPosition.width < totalWidth) {
            newElementPosition.x = Math.max(borderRadius, anchorBox.x - borderRadius - arrowOffset);
            horizontalAlignment = "left";
        } else if (newElementPosition.width + borderRadius * 2 < totalWidth) {
            newElementPosition.x = totalWidth - newElementPosition.width - borderRadius;
            horizontalAlignment = "right";
            // Position arrow accurately.
            var arrowRightPosition = Math.max(0, totalWidth - anchorBox.x - anchorBox.width - borderRadius - arrowOffset);
            arrowRightPosition += anchorBox.width / 2;
            this._popupArrowElement.style.right = arrowRightPosition + "px";
        } else {
            newElementPosition.x = borderRadius;
            newElementPosition.width = totalWidth - borderRadius * 2;
            newElementPosition.height += scrollerWidth;
            horizontalAlignment = "left";
            if (verticalAlignment === "bottom")
                newElementPosition.y -= scrollerWidth;
            // Position arrow accurately.
            this._popupArrowElement.style.left = Math.max(0, anchorBox.x - borderRadius * 2 - arrowOffset) + "px";
            this._popupArrowElement.style.left += anchorBox.width / 2;
        }

        this.element.className = "popover " + verticalAlignment + "-" + horizontalAlignment + "-arrow";
        this.element.positionAt(newElementPosition.x - borderWidth, newElementPosition.y - borderWidth);
        this.element.style.width = newElementPosition.width + borderWidth * 2 + "px";
        this.element.style.height = newElementPosition.height + borderWidth * 2 + "px";
    }
}

WebInspector.PopoverHelper = function(panelElement, getAnchor, showPopup, showOnClick, onHide)
{
    this._panelElement = panelElement;
    this._getAnchor = getAnchor;
    this._showPopup = showPopup;
    this._showOnClick = showOnClick;
    this._onHide = onHide;
    panelElement.addEventListener("mousedown", this._mouseDown.bind(this), false);
    panelElement.addEventListener("mousemove", this._mouseMove.bind(this), false);
}

WebInspector.PopoverHelper.prototype = {
    _mouseDown: function(event)
    {
        this._killHidePopupTimer();
        this._handleMouseAction(event, true);
    },

    _mouseMove: function(event)
    {
        // Pretend that nothing has happened.
        if (this._hoverElement === event.target || (this._hoverElement && this._hoverElement.isAncestor(event.target)))
            return;

        // User has 500ms to reach the popup.
        if (this._popup && !this._hidePopupTimer) {
            var self = this;
            function doHide()
            {
                self._hidePopup();
                delete self._hidePopupTimer;
            }
            this._hidePopupTimer = setTimeout(doHide, 500);
        }

        this._handleMouseAction(event);
    },

    _handleMouseAction: function(event, isMouseDown)
    {
        this._resetHoverTimer();

        this._hoverElement = this._getAnchor(event.target);
        if (!this._hoverElement)
            return;

        const toolTipDelay = isMouseDown ? 0 : (this._popup ? 600 : 1000);
        this._hoverTimer = setTimeout(this._mouseHover.bind(this, this._hoverElement), toolTipDelay);
    },

    _resetHoverTimer: function()
    {
        if (this._hoverTimer) {
            clearTimeout(this._hoverTimer);
            delete this._hoverTimer;
        }
    },

    hidePopup: function()
    {
        this._resetHoverTimer();
        this._hidePopup();
    },

    _hidePopup: function()
    {
        if (!this._popup)
            return;

        if (this._onHide)
            this._onHide();

        this._popup.hide();
        delete this._popup;
    },

    _mouseHover: function(element)
    {
        delete this._hoverTimer;

        this._popup = this._showPopup(element);
        if (this._popup)
            this._popup.contentElement.addEventListener("mousemove", this._killHidePopupTimer.bind(this), true);
    },

    _killHidePopupTimer: function()
    {
        if (this._hidePopupTimer) {
            clearTimeout(this._hidePopupTimer);
            delete this._hidePopupTimer;

            // We know that we reached the popup, but we might have moved over other elements.
            // Discard pending command.
            this._resetHoverTimer();
        }
    }
}