Source: ui/tooltip/TooltipComponent.js

// Copyright 2012 Tart. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS-IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

/**
 * @fileoverview
 */

goog.provide('tart.ui.TooltipComponent');
goog.require('goog.dom');
goog.require('goog.style');
goog.require('tart.ui.Component');
goog.require('tart.ui.TooltipComponentModel');



/**
 * @constructor
 * @extends {tart.ui.Component}
 */
tart.ui.TooltipComponent = function(refElement, options) {
    this.element = goog.dom.getElement(this.id);
    this.refElement = refElement;
    this.model = new this.modelClass(options);
    if (!this.element) {
        this.element = /** @type {Element} */(tart.dom.createElement(this.templates_base()));
        this.element.style.display = 'none';
        document.body.appendChild(this.element);
    }

    this.contentArea = goog.dom.getElementsByClass('content', this.element)[0];
    this.wrapper = goog.dom.getElementsByClass('wrapper', this.element)[0];
    this.cap = goog.dom.getElementsByClass('cap', this.element)[0];


    this.bindModelEvents();
    this.bindDomEvents();
};
goog.inherits(tart.ui.TooltipComponent, tart.ui.Component);

tart.ui.TooltipComponent.prototype.modelClass = tart.ui.TooltipComponentModel;

tart.ui.TooltipComponent.prototype.id = 'tartTooltip';
tart.ui.TooltipComponent.prototype.cssClass = 'tartTooltip';


/** @param {Element} refElement Reference element. */
tart.ui.TooltipComponent.prototype.setRefElement = function(refElement) {
    this.unbindDomEvents();
    this.refElement = refElement;
    this.bindDomEvents();
};


/** @override */
tart.ui.TooltipComponent.prototype.bindDomEvents = function() {
    switch (this.model.options.type) {
        case tart.ui.TooltipComponentModel.Type.CLICK:
            goog.events.listen(this.refElement, goog.events.EventType.CLICK, this.onClick, false, this);
            break;

        case tart.ui.TooltipComponentModel.Type.HOVER:
        default:
            goog.events.listen(this.refElement, [goog.events.EventType.MOUSEOVER, goog.events.EventType.MOUSEOUT],
                this.onHover, false, this);
            goog.events.listen(this.element, goog.events.EventType.MOUSEOUT, this.onBoxMouseout, false, this);
    }
};

tart.ui.TooltipComponent.prototype.onClick = function(e) {
    this.model.handleEvent(e.type);
    e.stopPropagation();
    this.bodyListen = goog.events.listen(document.body, goog.events.EventType.CLICK, function(e) {
        if (goog.dom.contains(this.element, e.target))
            return;
        goog.events.unlistenByKey(this.bodyListen);
        this.model.handleEvent(tart.ui.TooltipComponentModel.SMEventType.BODY_CLICK);
    }, false, this);
};

tart.ui.TooltipComponent.prototype.onHover = function(e) {
    if (e.type == goog.events.EventType.MOUSEOUT &&
        ((e.relatedTarget && goog.dom.contains(this.element, e.relatedTarget)) ||
        e.relatedTarget == this.element) || e.relatedTarget == null || goog.dom.contains(this.refElement, e.relatedTarget))
        return;
    this.model.handleEvent(e.type);

};

tart.ui.TooltipComponent.prototype.onBoxMouseout = function(e) {
    if (goog.dom.contains(this.element, e.relatedTarget)) {
        return false;
    }
    if (e.relatedTarget != this.refElement)
        this.model.handleEvent(e.type);
};


/** Unbind DOM event listeners of reference element. */
tart.ui.TooltipComponent.prototype.unbindDomEvents = function() {
    var clickListeners = goog.events.getListeners(this.refElement, goog.events.EventType.CLICK, false),
        mouseOverListeners = goog.events.getListeners(this.refElement, goog.events.EventType.MOUSEOVER, false),
        mouseOutListeners = goog.events.getListeners(this.refElement, goog.events.EventType.MOUSEOUT, false),
        listeners = goog.array.concat(clickListeners, mouseOverListeners, mouseOutListeners);

    goog.array.forEach(listeners, function(listener) {
        goog.events.unlistenByKey(listener);
    });
};


/** @override */
tart.ui.TooltipComponent.prototype.bindModelEvents = function() {
    goog.events.listen(this.model, tart.ui.TooltipComponentModel.EventType.SHOW, this.onShow, undefined, this);
    goog.events.listen(this.model, tart.ui.TooltipComponentModel.EventType.INIT, this.onInit, undefined, this);
    goog.events.listen(this.model, tart.ui.TooltipComponentModel.EventType.CLICK_WAIT, this.onWait, undefined,
        this);
};


tart.ui.TooltipComponent.prototype.onWait = function() {
    if (this.element.tooltip != this && this.element.tooltip) {
        this.element.tooltip.reset();
    }
    this.element.tooltip = this;
};


tart.ui.TooltipComponent.prototype.onShow = function() {
    this.contentArea.innerHTML = (this.templates_loading());
    document.body.appendChild(this.element);
    this.element.style.display = 'inline-block';
    this.position();

    this.windowResizeListener = goog.events.listen(window, goog.events.EventType.RESIZE, function(e) {
        this.position();
    }, false, this);
    this.windowScrollListener = goog.events.listen(window, goog.events.EventType.SCROLL, function(e) {
        this.position();
    }, false, this);
};

tart.ui.TooltipComponent.prototype.reset = function() {
    this.model.reset(); // sends to onInit
};

tart.ui.TooltipComponent.prototype.onInit = function() {
    this.element.style.display = 'none';
    goog.events.unlistenByKey(this.windowResizeListener);
    goog.events.unlistenByKey(this.windowScrollListener);
};


/**
 * This function returns the base of the tooltip as a string.
 * @return {string}
 */
tart.ui.TooltipComponent.prototype.templates_base = function() {
    return '<div id="' + this.id + '" class="' + this.cssClass + '">' +
        '<div class="wrapper">' +
        '<div class="content"></div>' +
        '<div class="cap"></div>' +
        '</div>' +
        '</div>';
};


/**
 * This function returns the content area of the tooltip as a string.
 * @return {string}
 */
tart.ui.TooltipComponent.prototype.templates_loading = function() {
    return '<div class="loadContainer"><div class="loading"></div></div>';
};


/**
 * This function takes a string or an element to append into the content area of the tooltip.
 * @param content {string | Element}
 */
tart.ui.TooltipComponent.prototype.setContent = function(content) {
    if (typeof content == 'string') {
        this.contentArea.innerHTML = content;
    }
    else {
        this.contentArea.innerHTML = '';
        this.contentArea.appendChild(content);
    }

    this.position();
};


/**
 * This method takes a reference element and uses its offSet and size values along with tooltip element's size values
 * to calculate the right position to display the tooltip. If the toolTip can be shown on the top of the reference
 * element, it positions the tooltip with the distance calculated by this.model's tipOffset and boxOffset values. If it
 * is not to possible to display on top, than it looks for the suitable area to display tooltip. The possible matches
 * are 'right', 'left', 'bottom' and as a default match 'top'.
 *
 **/
tart.ui.TooltipComponent.prototype.position = function() {
    var refElementOffset = goog.style.getPageOffset(this.refElement);
    var refElementSize = goog.style.getSize(this.refElement);
    var myElementSize = goog.style.getSize(this.element);
    var myWrapperSize = goog.style.getSize(this.wrapper);
    var myWindowSize = goog.dom.getViewportSize();
    var winScrollTop = document.body.scrollTop || window.document.documentElement.scrollTop;
    var winScrollLeft = document.body.scrollLeft || window.document.documentElement.scrollLeft;
    var coordinate;
    var handlerFn;

    var topDown = true;
    var horizontalShift = 0;
    var verticalShift = 0;
    var verticalTipCapShift = 0;
    var horizontalTipCapShift = 0;

    if (refElementSize.width < this.model.offsetThreshold) {
        this.model.tipOffset = refElementSize.width / 2;
    }

    if ((refElementOffset.x < winScrollLeft) &&
        (refElementOffset.x + refElementSize.width - winScrollLeft < 2 * this.model.tipOffset)) {
        topDown = false;
        this.model.options.direction = tart.ui.TooltipComponentModel.Direction.RIGHT;
        horizontalTipCapShift = -8;
    }

    if (myWindowSize.width + winScrollLeft - refElementOffset.x < 2 * this.model.tipOffset) {
        topDown = false;
        this.model.options.direction = tart.ui.TooltipComponentModel.Direction.LEFT;
        horizontalTipCapShift = myWrapperSize.width;
    }

    if (topDown) {
        if (refElementOffset.x < winScrollLeft) {
            horizontalShift = winScrollLeft - refElementOffset.x;
        }

        if (horizontalShift == 0) {
            if (myWrapperSize.width + (refElementOffset.x - winScrollLeft) > myWindowSize.width &&
                this.model.options.direction != tart.ui.TooltipComponentModel.Direction.TOP_LEFT) {
                horizontalShift = horizontalShift +
                    (myWindowSize.width - myWrapperSize.width - refElementOffset.x + winScrollLeft);
            }
        }

        if (refElementOffset.y - winScrollTop >= myElementSize.height + this.model.tipOffset + this.model.boxOffset) {
            if (this.model.options.direction != tart.ui.TooltipComponentModel.Direction.TOP_LEFT) {
                this.model.options.direction = tart.ui.TooltipComponentModel.Direction.TOP;
            }
            verticalTipCapShift = myWrapperSize.height;
        }
        else {
            this.model.options.direction = tart.ui.TooltipComponentModel.Direction.BOTTOM;
            verticalTipCapShift = -16;
        }

        if (this.model.options.direction == tart.ui.TooltipComponentModel.Direction.TOP_LEFT) {
            horizontalTipCapShift = myWrapperSize.width - 36;
        } else {
            horizontalTipCapShift = (horizontalShift >= 0) ? this.model.tipOffset :
                (-horizontalShift >= myWrapperSize.width - this.model.tipOffset) ?
                -horizontalShift : this.model.tipOffset - horizontalShift;
        }

        verticalShift = 0;
    }
    else {
        if (myWindowSize.height + winScrollTop - refElementOffset.y - myElementSize.height < 0) {
            verticalShift = myWindowSize.height + winScrollTop - refElementOffset.y - myElementSize.height;
        }
        if (refElementOffset.y <= winScrollTop) {
            verticalShift = winScrollTop - refElementOffset.y;
        }

        if (verticalShift >= 0) {
            verticalTipCapShift = this.model.tipOffset;
        }
        else {
            verticalTipCapShift = (this.model.tipOffset - verticalShift >= myElementSize.height - this.model.tipOffset) ?
                myElementSize.height - this.model.tipOffset : this.model.tipOffset - verticalShift;
        }

        if (this.model.tipOffset >= myElementSize.height / 2) {
            verticalTipCapShift = myElementSize.height / 2 - 1;
        }
    }

    this.wrapper.appendChild(this.cap);
    goog.dom.classlist.remove(this.element, 'right', 'left', 'top', 'bottom', 'topLeft');
    goog.dom.classlist.add(this.element, this.model.options.direction);
    this.cap.style.top = verticalTipCapShift + 'px';
    this.cap.style.left = horizontalTipCapShift + 'px';

    switch (this.model.options.direction) {
        case tart.ui.TooltipComponentModel.Direction.LEFT:
            handlerFn = this.positionLeft;
            break;
        case tart.ui.TooltipComponentModel.Direction.RIGHT:
            handlerFn = this.positionRight;
            break;
        case tart.ui.TooltipComponentModel.Direction.BOTTOM:
            handlerFn = this.positionBottom;
            break;
        case tart.ui.TooltipComponentModel.Direction.TOP:
            handlerFn = this.positionTop;
            break;
        case tart.ui.TooltipComponentModel.Direction.TOP_LEFT:
            handlerFn = this.positionTopLeft;
            break;
        default:
            handlerFn = this.positionTop;
            break;
    }

    coordinate = handlerFn.call(this, refElementOffset, refElementSize, myElementSize);
    this.element.style.top = coordinate.y + verticalShift + 'px';
    this.element.style.left = coordinate.x + horizontalShift + 'px';
};


/**
 * @protected
 *
 * @param refElementOffset {goog.math.Coordinate}
 * @param refElementSize {goog.math.Size}
 * @param myElementSize {goog.math.Size}
 * @return {goog.math.Coordinate}
 */
tart.ui.TooltipComponent.prototype.positionLeft = function(refElementOffset, refElementSize, myElementSize) {
    var y = refElementOffset.y - 16;
    var x = refElementOffset.x - (myElementSize.width);

    return new goog.math.Coordinate(x, y);
};


/**
 * @protected
 *
 * @param refElementOffset {goog.math.Coordinate}
 * @param refElementSize {goog.math.Size}
 * @param myElementSize {goog.math.Size}
 * @return {goog.math.Coordinate}
 */
tart.ui.TooltipComponent.prototype.positionTop = function(refElementOffset, refElementSize, myElementSize) {
    var y = refElementOffset.y - (myElementSize.height);
    var x = refElementOffset.x - 16;

    return new goog.math.Coordinate(x, y);
};


/**
 * @protected
 *
 * @param refElementOffset {goog.math.Coordinate}
 * @param refElementSize {goog.math.Size}
 * @param myElementSize {goog.math.Size}
 * @return {goog.math.Coordinate}
 */
tart.ui.TooltipComponent.prototype.positionTopLeft = function(refElementOffset, refElementSize, myElementSize) {
    var y = refElementOffset.y - (myElementSize.height);
    var x = refElementOffset.x - (myElementSize.width) + 80;

    return new goog.math.Coordinate(x, y);
};


/**
 * @protected
 *
 * @param refElementOffset {goog.math.Coordinate}
 * @param refElementSize {goog.math.Size}
 * @param myElementSize {goog.math.Size}
 * @return {goog.math.Coordinate}
 */
tart.ui.TooltipComponent.prototype.positionBottom = function(refElementOffset, refElementSize, myElementSize) {
    var y = refElementOffset.y + refElementSize.height;
    var x = refElementOffset.x - 16;

    return new goog.math.Coordinate(x, y);
};


/**
 * @protected
 *
 * @param refElementOffset {goog.math.Coordinate}
 * @param refElementSize {goog.math.Size}
 * @param myElementSize {goog.math.Size}
 * @return {goog.math.Coordinate}
 */
tart.ui.TooltipComponent.prototype.positionRight = function(refElementOffset, refElementSize, myElementSize) {
    var y = refElementOffset.y - 16;
    var x = refElementOffset.x + refElementSize.width;

    return new goog.math.Coordinate(x, y);
};