Skip to content
Snippets Groups Projects
tooltip.js 6.59 KiB
Newer Older
import CSS from './css.js';

/**
 * Tooltip library for Stud.IP
 *
 * @author Jan-Hendrik Willms <tleilax+studip@gmail.com>
 * @copyright Stud.IP Core Group 2014
 * @license GPL2 or any later version
 * @since Stud.IP 3.1
 */

let count = 0;
let threshold = 0;

class Tooltip {
    static get count() {
        return count;
    }

    static set count(value) {
        count = value;
    }

    // Threshold used for "edge detection" (imagine a padding along the edges)
    static get threshold() {
        return threshold;
    }

    static set threshold(value) {
        threshold = value;
    }

    /**
     * Returns a new unique id of a tooltip.
     *
     * @return {string} Unique id
     * @static
     */
    static getId() {
        const id = `studip-tooltip-${Tooltip.count}`;
        Tooltip.count += 1;
        return id;
    }

    /**
     * Constructs a new tooltip at given location with given content.
     * The applied css class may be changed by the fourth parameter.
     *
     * @class
     * @classdesc Stud.IP tooltips provide an improved layout and handling
     *            of contents (including html) than the browser's default
     *            tooltip through title attribute would
     *
     * @param {int} x - Horizontal position of the tooltip
     * @param {int} y - Vertical position of the tooltip
     * @param {string} content - Content of the tooltip (may be html)
     * @param {string} css_class - Optional name of the applied css class /
     *                             defaults to 'studip-tooltip'
     */
    constructor(x, y, content, css_class) {
        // Obtain unique id of the tooltip
        this.id = Tooltip.getId();

        // Create dom element of the tooltip, apply id and class and attach
        // to dom
        this.element = $('<div>');
        this.element.addClass(css_class || 'studip-tooltip');
        this.element.attr('id', this.id);
        this.element.attr('role', 'tooltip');
        this.element.appendTo('body');

        // Set position and content and paint the tooltip
        this.position(x, y);
        this.update(content);
        this.paint();
    }

    /**
     * Translates the arrow(s) under a tooltip using css3 translate
     * transforms. This is needed at the edges of the screen.
     * This implies that a current browser is used. The translation could
     * also be achieved by adjusting margins but that way we would need
     * to hardcode values into this function since it's a struggle to
     * obtain the neccessary values from the CSS pseudo selectors in JS.
     *
     * Internal, css rules are dynamically created and applied to the current
     * document by using the methods provided in the file studip-css.js.
     *
     * @param {int} x - Horizontal offset
     * @param {int} y - Vertical offset
     */
    translateArrows(x, y, left_arrow = false) {
        CSS.removeRule(`#${this.id}::before`);
        CSS.removeRule(`#${this.id}::after`);

        if (x !== 0 || y !== 0) {
            let before_rule = {
                transform: `translate(${x}px, ${y}px);`
            };
            if (left_arrow) {
                before_rule.transform = `translate(${x}px, ${y}px) rotate(90deg);`;
            }
            let after_rule = before_rule;
            if (left_arrow) {
                after_rule['border-width'] = '9px';
            }
            CSS.addRule(`#${this.id}::before`, before_rule, ['-ms-', '-webkit-']);
            CSS.addRule(`#${this.id}::after`, after_rule, ['-ms-', '-webkit-']);
        }
    }

    /**
     * Updates the position of the tooltip.
     *
     * @param {int} x - Horizontal position of the tooltip
     * @param {int} y - Vertical position of the tooltip
     */
    position(x, y) {
        this.x = x;
        this.y = y;
    }

    /**
     * Updates the contents of the tooltip.
     *
     * @param {string} content - Content of the tooltip (may be html)
     */
    update(content) {
        this.element.html(content);
    }

    /**
     * "Paints" the tooltip. This method actually computes the dimensions of
     * the tooltips, checks for screen edges and calculates the actual offset
     * in the current document.
     * This method is neccessary due to the fact that position and content
     * can be changed apart from each other.
     * Thus: Don't forget to repaint after adjusting any of the two.
     */
    paint() {
        const width = this.element.outerWidth(true);
        const height = this.element.outerHeight(true);
        const maxWidth = $(document).width();
        const maxHeight = $(document).height();
        let x = this.x - width / 2;
        let y = this.y - height;
        //The arrow offset is the offset from the bottom right corner of
        //the tooltip "frame".
        let arrow_offset_x = 0;
        let arrow_offset_y = 0;
        let left_arrow = false;

        if (y < 0) {
            y = 0;
            x = this.x + 20;
            //Put the arrow on the left side and move the tooltip,
            //if there is still enough place left on the right.
            left_arrow = true;
            arrow_offset_y = -height + this.y + 10;
            if (arrow_offset_y > -20) {
                y+= arrow_offset_y + 20;
                arrow_offset_y = -20;
            }
            arrow_offset_x = -width / 2 - 8;
        } else if (y + height > maxHeight) {
            y = maxHeight - height;

        if (x < 0) {
            arrow_offset_x = 0;
            x = 0;
        } else if (x + width > maxWidth) {
            arrow_offset_x = x + width - maxWidth;
            x = maxWidth - width;
        }
        this.translateArrows(arrow_offset_x, arrow_offset_y, left_arrow);

        this.element.css({
            left: x,
            top: y
        });
    }

    /**
     * Toggles the visibility of the tooltip. If no state is provided,
     * the tooltip will be hidden if visible and vice versa. Pretty straight
     * forward and no surprises here.
     * This method implicitely calls paint before a tooltip is shown (in case
     * it was forgotten).
     *
     * @param {bool} visible - Optional visibility parameter to set the
     *                         tooltip to a certain state
     */
    toggle(visible) {
        if (visible) {
            this.paint();
        }
        this.element.toggle(visible);
    }

    /**
     * Reveals the tooltip.
     *
     * @see Tooltip.toggle
     */
    show() {
        this.toggle(true);
    }

    /**
     * Hides the tooltip.
     *
     * @see Tooltip.toggle
     */
    hide() {
        this.toggle(false);
    }

    /**
     * Removes the tooltip
     */
    remove() {
        this.element.remove();
    }
}

export default Tooltip;