1
\$\begingroup\$

Tonight I decided to revamp some code I wrote a few months ago. One part of that was to do with showing (and hiding) tooltips. The functions are called with:

jQuery(element).showTooltip('HTML');
jQuery(element).hideTooltip();

The old code

$.fn.showTooltip = function(text)
{
    /* Remove any existing tooltip. */
    $(this).removeTooltip();

    /* Generate a unique reference for the tooltip. */
    var ref = $(this).attr('name');

    /* Create the tooltip and add it after the element. */
    $tooltip = $('<span class="tooltip" data-ref="' + ref + '"></span>')
        .html(text)
        .css('display','none')
        .insertAfter($(this))
        .fadeIn(animationSpeed);
}

$.fn.removeTooltip = function()
{
    var ref = $(this).attr('name');

    /* Remove the tooltip. */
    $('span[data-ref="' + ref + '"]')
        .stop()
        .fadeOut(animationSpeed, function() {
        $(this).remove();
    });
}

This code did the trick, but the were a few occasions where a tooltip either wouldn't appear or disappear when required. Equally the animations were a bit off.

The new revamped code

$.fn.showTooltip = function(html) {
    var

        /* Create a unique reference to the element to be used in the
         * tooltip. */
        tooltipRef = $(this).attr('name'),

        /* Create a reference to any current tooltip for the element. */
        existingTooltip = $('[data-ref="' + tooltipRef + '"]'),

        /* Create a variable to be used to hold the new tooltip. */
        $tooltip

    ;   

    /* If existingTooltip doesn't match an existing object then create
     * the new tooltip object, asigning its HTML and hiding it before
     * inserting it after the element. */
    if(typeof existingTooltip != 'object'
        || typeof existingTooltip.length != 'number'
        || existingTooltip.length === 0)
        $tooltip = $('<span class="tooltip"></span>')
                    .attr('data-ref', tooltipRef)
                    .html(html)
                    .hide()
                    .insertAfter($(this));

    /* If existingTooltip is visible and that contains the same html
     * which is being passed in then there is no need to recreate. */
    else if(existingTooltip.is(':visible')
        && existingTooltip.html() === html)
        return;

    /* If existingTooltip is visible but does not contain the same html
     * then hide existingTooltip then set its HTML and set $tooltip as
     * existingTooltip. */
    else if(existingTooltip.is(':visible'))
    {
        $(this).hideTooltip();
        $tooltip = existingTooltip;
        $tooltip.queue(function() {
            $tooltip.html(html);
            $(this).dequeue();
        });
    }

    /* If existingTooltip isn't visible but contains the same HTML, set
     * $tooltip as existingTooltip. */
    else if(existingTooltip.html() === html)
        $tooltip = existingTooltip;

    /* If existingTooltip isn't visible and doesn't contain the same
     * HTML, set its HTML and set $tooltip as existingTooltip. */
    else if(existingTooltip.html() !== html)
    {
        existingTooltip.html(html);
        $tooltip = existingTooltip;
    }

    /* If none of the above applies then something has gone wrong. */
    else
        return;

    /* Ensure any current animation isn't occuring before displaying
     * the new (or modified) tooltip. */
    $tooltip.queue(function() {
        $(this)
            .stop()
            .fadeIn(global.animationSpeed);
        $(this).dequeue();
    });
};

$.fn.hideTooltip = function() {
    var
        /* Get the unique reference from the element to be used to
         * get the tooltip object. */
        tooltipRef = $(this).attr('name'),

        /* Use the above reference to get the tooltip object. */
        $tooltip = $('span[data-ref="' + tooltipRef + '"]')
    ;

    /* If there is no tooltip then there is no need to proceed. */
    if(typeof $tooltip != 'object'
        || typeof $tooltip.length != 'number'
        || $tooltip.length === 0
        || !$tooltip.is(':visible'))
        return;

    /* Hide the tooltip. */
    $tooltip
        .stop()
        .fadeOut(global.animationSpeed);
}

The main flaw that I'm currently aware of with this (which I'll sort out tomorrow) is that the element currently requires a name in order for the tooltip to work.

I think I've covered all bases with the large if statement within the showTooltip function, but I'm not sure if maybe I've gone too overboard with it. Is there any way I could condense the code?

I'm certainly no JavaScript expert, so any criticism would be very welcome!

Here's a JSFiddle example: http://jsfiddle.net/UPAXV/

Many thanks.

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

You can check out the optimized code at work here

CSS: removed positioning and moved it to JS to make it dynamic

.tooltip {
    background:#000;
    color:#fff;
    padding:2px 5px;
    line-height:20px;
}

JS: in the comments

//protect your code inside an immediate function
//that way, you can get away with all the crazy stuff you do
//while protecting yourself from all the crazy stuff other codes do
;(function (window, document, $, undefined) {

    //for constants, I'd declare then up top so they are easily configurable
    var animationSpeed = 250  //this one's for animation speed
      , fn = {}               //this is for our methods, explained later
      , offset = 12           //the offset caused by the triangle
      , dataName = 'tooltip'  //our data name
      ;

    //our tooltip methods

    fn.show = function (html) {

        //cache frequently used values to avoid refetching
        var element = $(this)
          , tooltip
          , offset
          , visible
          , same
          ;

        //this condition will return true if there was no data found
        //thus no tooltip was created beforehand
        if (!(tooltip = element.data(dataName))) {

            //so we create one and reference it to tooltip variable
            //we won't rely on CSS for style and dynamically calculate
            //the tooltip's position upon attachment
            tooltip = $('<span class="tooltip" />')
                        .hide()
                        .insertAfter(element)
                        .css({
                          position: 'absolute',
                          left: offset.left + element.outerWidth(true) + 12,
                          top: offset.top
                        });

            //we use jQuery's data() to avoid circular references
            //http://stackoverflow.com/q/10004593/575527
            element.data(dataName, tooltip);
        }


        visible = tooltip.is(':visible');
        same = tooltip.text() === html;

        //beware of single-line if statements. althout it looks cleaner
        //but never forget the semicolon or you'll run into problems

        //visible     same     - do nothing
        //visible     not same - fade out, change, fade in
        //not visible not same - change, fade in
        //not visible same     - fade in

        if (visible && same) return;
        if (visible) tooltip.fadeOut(animationSpeed);
        if (!same) {
          tooltip.queue(function () {
            $(this).text(html).dequeue();
          });
        }

        tooltip.queue(function () {
          $(this).stop().fadeIn(animationSpeed).dequeue();
        });

    }

    fn.hide = function () {
        var tooltip;
        //so we check if there is an existing tooltip 
        //and if that existing tooltip is visible
        if ((tooltip = $(this).data(dataName)) && tooltip.is(':visible')) tooltip.fadeOut(animationSpeed);
    }

    //now I have reformatted the tooltip to only use one name to avoid
    //possible collisions with other plugins from other developers
    //so calling targets.tooltip('show',arg1,arg2,...,argN) is the same as
    //target.show(arg1,arg2,...,argN) for each element in the set
    $.fn.tooltip = function (event) {

            //get the selected method to run
        var toExecute = fn[event],
            //remove the first argument since it only determines the method to use
            args = Array.prototype.slice.call(arguments, 1);

        //check if the method exists, return if non-existent
        if (typeof toExecute !== 'function') return;

        //run the method for each of the elements in the set
        $(this).each(function () {
            toExecute.apply(this, args);
        });
    }

}(this, document, jQuery));

//usage
$(function () {
    $('button#btShow').on('click', function () {
        $('input').tooltip('show', Math.random());
    });
    $('button#btHide').on('click', function () {
        $('input').tooltip('hide');
    })
});
\$\endgroup\$
6
  • \$\begingroup\$ I had hard-coded the CSS for flexibility (not necessarily forcing the tooltip to display in a certain way). I'll take the .data() suggestion on board, I wasn't aware of any circular reference problem. Could you explain what the the top bit is doing (;(function (window, document, $, undefined))? The only issue with this is that the tooltip doesn't redisplay itself when you click Show more than once - I probably should have made that a bit clearer before though! Thanks! \$\endgroup\$ Commented Apr 12, 2013 at 7:45
  • 1
    \$\begingroup\$ That bit is part of the IIFE (Immediately Invoked Function Expression), used to create an enclosed scope for your script. Call it your "private scope". Basically it's function expression that's immediately called, invoked at the bottom with (this, document, jQuery) as window, document and jQuery respectively. \$\endgroup\$
    – Joseph
    Commented Apr 12, 2013 at 7:49
  • 1
    \$\begingroup\$ @JamesDonnelly Updated the script. Now it redisplays the tooltip when show is triggered more than once. \$\endgroup\$
    – Joseph
    Commented Apr 12, 2013 at 15:24
  • \$\begingroup\$ Ah awesome! Is there a reason for the "undefined" in ;(function (window, document, $, undefined) { ...? Also, would I add more functions to this particular wrapper or would I have separate (function (... wrappers for each? Also, is there supposed to be a ; at the start? \$\endgroup\$ Commented Apr 12, 2013 at 16:04
  • 1
    \$\begingroup\$ @JamesDonnelly because undefined is like a variable in JS, and not a constant. It's value can be changed. Thus, we create a true undefined in our scope by passing nothing as the 4th argument, making it truly undefined. The ; at the beginning is to guard the our scope, especially when a function comes before it which makes JS think the starting ( invokes it. To add functionality, just add to the fn collection. \$\endgroup\$
    – Joseph
    Commented Apr 12, 2013 at 20:58

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.