Source: tooltiper.js

/***
 * The only plugin's dependency - jQuery. It is required before
 * creating the plugin.
 */
var jQuery = require("jquery");

/**
 * There is an IIFE around the plugin.
 * The IIFE contains several helper functions.
 * The plugin gets assigned to `$.fn.tooltiper`.
 * @namespace TooltiperIIFE
 * @author Curveball <x.curveball.x@gmail.com>
 * @license MIT
 */
(function( $, window, document, undefined ) {
    "use strict";
    $.fn._init = $.fn.init;

    /**
     * Makes possible to get selector string later by attaching it to the jQuery object.
     * Wrapper around original init() function.
     * @function TooltiperIIFE~init
     * @param {string|*} selector selector string.
     * @param {object} context an object serving as context for selectors search. The match will be searched for in its children instead of the entire page.
     * @param {object} root usually $(document).
     * @returns {object} a jQuery object.
     */
    $.fn.init = function( selector, context, root ) {
        return (typeof selector === 'string') ? new $.fn._init(selector, context, root).data('selector', selector) : new $.fn._init(selector, context, root);
    };

    /**
     * Gets selector string passed to the plugin.
     * @function TooltiperIIFE~getSelector
     * @returns {string} selector string passed to the plugin.
     */
    $.fn.getSelector = function() {
        return $(this).data('selector');
    };

    /**
     * The plugin function assigned to `$.fn.tooltiper`.
     * Below go inner helper functions inside the plugin.
     * @namespace Tooltiper
     * @author Curveball <x.curveball.x@gmail.com>
     * @license MIT
     * @param {object.<string, *>} [options] object passed to the plugin upon calling.
     * @returns {object} returns collection of matched elements.
     */
    $.fn.tooltiper = function( options ) {

      var settings = $.extend( true, {}, $.fn.tooltiper.settings, options );

      var selector = $( this ).getSelector();

      if ( !areSettingsValid( settings ) || !selector ) return;

      /*** Events setup */
      $("body").on( "mouseenter", selector, function( event ) {
          showToolTip( $(this), event );
      }).on( "mouseleave", selector, function( event ) {
          hideToolTip( $(this) );
      });

      if ( settings.tooltipBound === 'cursor' ) {
          $( "body" ).on( "mousemove", selector, function( event ) {
              moveToolTip( $(this), event );
          });
      }

      /**
       * Checks whether the settings are valid.
       * @function Tooltiper~areSettingsValid
       * @param {object} [settings] all the settings for the plugin.
       * @param {string} [settings.tooltipType=text] - plain text or html.
       * @param {string} [settings.tooltipAppearenceMode=fadeIn] - effect's name.
       * @param {string} [settings.tooltipDisappearenceMode=fadeOut] - effect's name.
       * @param {number} [settings.tooltipOffset=10] - distance between tooltip and element, in pixels.
       * @param {string} [settings.tooltipBound=element] - is it bound to the element or the cursor.
       * @param {string} [settings.tooltipShowSpeed=fast] - speed like 'fast'.
       * @param {string} [settings.tooltipHideSpeed=fast] - speed like 'fast'.
       * @param {string} [settings.tooltipClass=js-tooltiper] - defaults to 'js-tooltiper'.
       * @param {string} [settings.tooltipElement=span] - defaults to span.
       * @param {object.<string, string>} [settings.tooltipCss] - css key/value pairs styles, see the defaults in the code below.
       * @returns {boolean} whether the settings are valid.
       */
      function areSettingsValid( settings ) {
          var errs = [];

          if (!$.isNumeric(settings.tooltipOffset)) errs.push(new Error('Settings.tooltipOffset option should be of type Number!'));

          if (settings.tooltipType.toLowerCase() !== 'text' && settings.tooltipType.toLowerCase() !== 'html') errs.push(new Error('Settings.tooltipType option should be equal to either "text" or "html"!'));

          if (typeof(settings.tooltipClass).toLowerCase() !== 'string') errs.push(new Error('Settings.tooltipOffset option should be of type String!'));

          if (typeof(settings.tooltipElement).toLowerCase() !== 'string') errs.push(new Error('Settings.tooltipElement option should be of type String and contain tag name!'));

          if (!$.isNumeric(settings.tooltipShowSpeed) && !~$.inArray(settings.tooltipShowSpeed.toLowerCase(), ['fast', 'normal', 'slow'])) errs.push(new Error('Settings.tooltipShowSpeed option should be of type Number or equal to "fast", "normal" or "slow"!'));

          if (!$.isNumeric(settings.tooltipHideSpeed) && !~$.inArray(settings.tooltipHideSpeed.toLowerCase(), ['fast', 'normal', 'slow'])) errs.push(new Error('Settings.tooltipHideSpeed option should be of type Number or equal to "fast", "normal" or "slow"!'));

          if (!~$.inArray(settings.tooltipAppearenceMode, ['show', 'fadeIn', 'slideDown'])) errs.push(new Error('Settings.tooltipAppearenceMode option should be of type Number or equal to "show", "fadeIn" or "slideDown"!'));

          if (!~$.inArray(settings.tooltipDisappearenceMode, ['hide', 'fadeOut', 'slideUp'])) errs.push(new Error('Settings.tooltipDisappearenceMode option should be of type Number or equal to "hide", "fadeOut" or "slideUp"!'));

          if (settings.tooltipBound.toLowerCase() !== 'element' && settings.tooltipBound.toLowerCase() !== 'cursor') errs.push(new Error('Settings.tooltipBound option should be equal to either "element" or "cursor"!'));

          if ((typeof settings.tooltipCss).toLowerCase() !== 'object') errs.push(new Error('Settings.tooltipCss option should be an object containing property-value pairs like {"property": "value", ...}!'));

          showError( errs );

          return errs.length ? false : true;
      }

      /**
       * Displays errors in the console.
       * @function Tooltiper~showError
       * @param {Error[]} errs array of Errors to show.
       * @returns {undefined}
       */
      function showError( errs ) {
          for(var i = 0; i < errs.length; i++) {
              console.error('Tooltiper did nothing because an error occured! ' + errs[i].message);
          }
      }

      /**
       * Moves the tooltip.
       * @function Tooltiper~moveToolTip
       * @param {object} element selected element for tooltip, jQuery object.
       * @param {MouseEvent} event mousemove event when cursor goes over the element.
       * @returns {undefined}
       */
      function moveToolTip( element, event ) {
          var tooltip = getToolTip(element);
          if (!tooltip.data('tooltiperCanMove')) return;

          var positionedParent = $(getPositionedParent( element )),
              tooltipX = positionedParent.length ? event.pageX - positionedParent.offset().left : event.pageX;

          tooltip.animate( {"left": tooltipX}, 1000/60, 'swing' );
      }

      /**
       * Cleans up a bit after the tooltip has been shown.
       * @function Tooltiper~resetToolTip
       * @param {object} element selected element for tooltip, jQuery object.
       * @returns {string} the tooltip's content.
       */
      function resetToolTip( element ) {
          element.data( 'tooltiperStop', element.data('tooltiperStop') ? false : true );

          element.next( settings.tooltipElement + "." + settings.tooltipClass ).remove();

          element.attr( 'title', element.data("tooltiperTitle") );

          return element.attr( 'title' );
      }

      /**
       * Shows the tooltip around the element.
       * @function Tooltiper~showToolTip
       * @param {object} element selected element for tooltip, jQuery object.
       * @param {MouseEvent} event mouseenter event when cursor gets over the element.
       * @returns {undefined}
       */
      function showToolTip( element, event ) {
          if ( $(event.target).hasClass( settings.tooltipClass ) ) return;

          var title = element.attr( 'title' );
          element.data( 'tooltiperStop', false );

          if ( isToolTipShown(element) ) title = resetToolTip( element );
          if ( !title ) return;

          element.data( "tooltiperTitle", title ).attr( 'title', "" );
          var tooltip = createToolTip( title );
          tooltip.data( 'tooltiperCanMove', true );

          setTooltipWidth( tooltip );
          setTooltipCoords( event, element, tooltip );
          element.after( tooltip );
          tooltip[ settings.tooltipAppearenceMode ]( settings.tooltipShowSpeed );
      }

      /**
       * Hides the tooltip.
       * @function Tooltiper~hideToolTip
       * @param {object} element selected element for tooltip, jQuery object.
       * @returns {undefined}
       */
      function hideToolTip( element ) {
          var title = element.data( "tooltiperTitle" );
          if ( !title || !isToolTipShown( element ) ) return;

          getToolTip( element )[ settings.tooltipDisappearenceMode ]( settings.tooltipHideSpeed, function() {
              if ( element.data( 'tooltiperStop' ) ) {
                  element.data( 'tooltiperStop', false );
                  return;
              }

              element.next( settings.tooltipElement + "." + settings.tooltipClass ).remove();
              element.attr( 'title', title );
              element.removeData( "tooltiperTitle" );
          });
      }

      /**
       * Creates tooltip element.
       * @function Tooltiper~createToolTip
       * @param {string} title what will be shown in the tooltip, the content of title attribute.
       * @returns {object} tooltip element, jQuery object.
       */
      function createToolTip( title ) {
          return $( "<" + settings.tooltipElement + ">" ).addClass( settings.tooltipClass )[ settings.tooltipType ]( title ).css( settings.tooltipCss );
      }

      /**
       * Gets tooltip.
       * @function Tooltiper~getToolTip
       * @param {object} element selected element for tooltip, jQuery object.
       * @returns {object} tooltip element, jQuery object.
       */
      function getToolTip( element ) {
          return element.next( settings.tooltipElement + "." + settings.tooltipClass );
      }

      /**
       * Returns tooltip's dimensions.
       * @function Tooltiper~getToolTipDimensions
       * @param {object} tooltip tooltip element, jQuery object.
       * @returns {object.<string, number>} tooltip's dimensions.
       */
      function getToolTipDimensions( tooltip ) {
          var clonedTooltip = tooltip.clone().off().css( {"position": "fixed",  "display": "block", "z-index": -9999, "visibility": "hidden", "right": 0, "bottom": 0} );

          clonedTooltip.appendTo( "body" );
          var tooltipHeight = clonedTooltip.outerHeight(),
              tooltipWidth = clonedTooltip.outerWidth();

          clonedTooltip.remove();

          return { width: tooltipWidth, height: tooltipHeight };
      }

      /**
       * Whether the tooltip is shown or not.
       * @function Tooltiper~isToolTipShown
       * @param {object} element tooltip element, jQuery object.
       * @returns {boolean} Whether the tooltip is shown.
       */
      function isToolTipShown( element ) {
         return element.next( settings.tooltipElement + "." + settings.tooltipClass ).length !== 0;
      }

      /**
       * Sets tooltip coordinates.
       * @function Tooltiper~setTooltipCoords
       * @param {MouseEvent} event mouseenter event when the cursor gets over the element.
       * @param {object} element selected element for tooltip, jQuery object.
       * @param {object} tooltip tooltip element, jQuery object.
       * @returns {undefined}
       */
      function setTooltipCoords( event, element, tooltip ) {
          var positionedParent = $( getPositionedParent( element ) ),
              tooltipWidth = tooltip.outerWidth();

          var elementOffsetTop = element.offset().top - $( window ).scrollTop();
          var elementHeight = element.outerHeight();
          var elementOffsetBottom = $( window ).height() - elementHeight - elementOffsetTop;

          if ( elementOffsetTop > elementOffsetBottom ) {
              var bottom = positionedParent.length ? positionedParent.outerHeight() - element.position().top + settings.tooltipOffset : $( window ).height() - element.position().top + settings.tooltipOffset;
              tooltip.css( {"bottom": bottom} );
          } else {
              tooltip.css( {"top": element.position().top +   settings.tooltipOffset + elementHeight} );
          }

          var pointOfMouseEntryX = positionedParent.length ? event.pageX - positionedParent.offset().left : event.pageX;

          var pointOfMouseEntryOffsetLeft = positionedParent.length ? pointOfMouseEntryX + positionedParent.offset().left - $( window ).scrollLeft() : pointOfMouseEntryX - $( window ).scrollLeft();

          var pointOfMouseEntryOffsetRight = $( window ).width() - pointOfMouseEntryOffsetLeft,
              diff = tooltipWidth - pointOfMouseEntryOffsetRight,
              toolTipXCoord = pointOfMouseEntryX;

          if ( diff > 0 ) {
              tooltip.data( 'tooltiperCanMove', false );
              toolTipXCoord -= diff + settings.tooltipOffset/2;
              if ( diff > pointOfMouseEntryOffsetLeft ) {
                  toolTipXCoord = toolTipXCoord + (diff - pointOfMouseEntryOffsetLeft) + settings.tooltipOffset;

                  tooltip.css( {"width": pointOfMouseEntryOffsetLeft + pointOfMouseEntryOffsetRight - settings.tooltipOffset} );
              }
          }
          tooltip.css( {"left": toolTipXCoord} );
      }

      /**
       * Sets tooltip width.
       * @function Tooltiper~setTooltipWidth
       * @param {object} tooltip tooltip element, jQuery object.
       * @returns {undefined}
       */
      function setTooltipWidth( tooltip ) {
          var maxWidth = parseFloat( tooltip.css("max-width") ),
              tooltipDimensions = getToolTipDimensions( tooltip );

          if ( tooltipDimensions.width < maxWidth ) {
              tooltip.css( {"width": tooltipDimensions.width + 1} );
          } else {
              tooltip.css( {"width": maxWidth} );
          }
      }

      /**
       * Gets element's positioned parent.
       * @function Tooltiper~getPositionedParent
       * @param {object} element selected element for tooltip, jQuery object.
       * @returns {object} element's positioned parent.
       */
      function getPositionedParent( element ) {
          var parents = element.parents(),
              positions = ['absolute', 'relative', 'fixed'],
              positionedParent = null;

          $.each( parents, function(index, parent) {
              if ( ~$.inArray( $.trim( $( parent ).css( "position" ) ), positions ) ) {
                  positionedParent = parent;
                  return false;
              }
          });
          return positionedParent;
      }

      return this;
    }

    /*** Plugin's default settings*/
    $.fn.tooltiper.settings = {
        tooltipType: 'text',
        tooltipAppearenceMode: 'fadeIn',
        tooltipDisappearenceMode: 'fadeOut',
        tooltipOffset: 10,
        tooltipBound: 'element',
        tooltipShowSpeed: 'fast',
        tooltipHideSpeed: 'fast',
        tooltipClass: "js-tooltiper",
        tooltipElement: "span",
        tooltipCss: {"display": "none", "max-width": "250px", "box-sizing": "border-box", "word-wrap": "break-word", "color": "black", "font-size": ".8em", "position": "absolute", "z-index": 9999, "background-color": "white", "padding": ".5em", "box-shadow": "0px 0px 4px 0px rgba(0,0,0,0.5)"}
    };
})(jQuery, window, document);