File "tooltipster.bundle.js"

Full Path: /home/vantageo/public_html/cache/cache/cache/cache/cache/cache/.wp-cli/wp-content/plugins/woocommerce-products-filter/js/tooltip/js/tooltipster.bundle.js
File size: 167.73 KB
MIME-type: text/plain
Charset: utf-8

/**
 * tooltipster http://iamceege.github.io/tooltipster/
 * A rockin' custom tooltip jQuery plugin
 * Developed by Caleb Jacob and Louis Ameline
 * MIT license
 */
"use strict";
(function (root, factory) {
    if (typeof define === 'function' && define.amd) {
        // AMD. Register as an anonymous module unless amdModuleId is set
        define(["jquery"], function (a0) {
            return (factory(a0));
        });
    } else if (typeof exports === 'object') {
        // Node. Does not work with strict CommonJS, but
        // only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory(require("jquery"));
    } else {
        factory(jQuery);
    }
}(this, function ($) {

// This file will be UMDified by a build task.

    var defaults = {
        animation: 'fade',
        animationDuration: 350,
        content: null,
        contentAsHTML: false,
        contentCloning: false,
        debug: true,
        delay: 300,
        delayTouch: [300, 500],
        functionInit: null,
        functionBefore: null,
        functionReady: null,
        functionAfter: null,
        functionFormat: null,
        IEmin: 6,
        interactive: false,
        multiple: false,
        // will default to document.body, or must be an element positioned at (0, 0)
        // in the document, typically like the very top views of an app.
        parent: null,
        plugins: ['sideTip'],
        repositionOnScroll: false,
        restoration: 'none',
        selfDestruction: true,
        theme: [],
        timer: 0,
        trackerInterval: 500,
        trackOrigin: false,
        trackTooltip: false,
        trigger: 'hover',
        triggerClose: {
            click: false,
            mouseleave: false,
            originClick: false,
            scroll: false,
            tap: false,
            touchleave: false
        },
        triggerOpen: {
            click: false,
            mouseenter: false,
            tap: false,
            touchstart: false
        },
        updateAnimation: 'rotate',
        zIndex: 9999999
    },
            // we'll avoid using the 'window' global as a good practice but npm's
            // jquery@<2.1.0 package actually requires a 'window' global, so not sure
            // it's useful at all
            win = (typeof window != 'undefined') ? window : null,
            // env will be proxied by the core for plugins to have access its properties
            env = {
                // detect if this device can trigger touch events. Better have a false
                // positive (unused listeners, that's ok) than a false negative.
                // http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
                hasTouchCapability: !!(
                        win
                        && ('ontouchstart' in win
                                || (win.DocumentTouch && win.document instanceof win.DocumentTouch)
                                || win.navigator.maxTouchPoints
                                )
                        ),
                hasTransitions: transitionSupport(),
                IE: false,
                // don't set manually, it will be updated by a build task after the manifest
                semVer: '4.2.7',
                window: win
            },
            core = function () {

                // core variables

                // the core emitters
                this.__$emitterPrivate = $({});
                this.__$emitterPublic = $({});
                this.__instancesLatestArr = [];
                // collects plugin constructors
                this.__plugins = {};
                // proxy env variables for plugins who might use them
                this._env = env;
            };

// core methods
    core.prototype = {

        /**
         * A function to proxy the public methods of an object onto another
         *
         * @param {object} constructor The constructor to bridge
         * @param {object} obj The object that will get new methods (an instance or the core)
         * @param {string} pluginName A plugin name for the console log message
         * @return {core}
         * @private
         */
        __bridge: function (constructor, obj, pluginName) {

            // if it's not already bridged
            if (!obj[pluginName]) {

                var fn = function () {};
                fn.prototype = constructor;

                var pluginInstance = new fn();

                // the _init method has to exist in instance constructors but might be missing
                // in core constructors
                if (pluginInstance.__init) {
                    pluginInstance.__init(obj);
                }

                $.each(constructor, function (methodName, fn) {

                    // don't proxy "private" methods, only "protected" and public ones
                    if (methodName.indexOf('__') != 0) {

                        // if the method does not exist yet
                        if (!obj[methodName]) {

                            obj[methodName] = function () {
                                return pluginInstance[methodName].apply(pluginInstance, Array.prototype.slice.apply(arguments));
                            };

                            // remember to which plugin this method corresponds (several plugins may
                            // have methods of the same name, we need to be sure)
                            obj[methodName].bridged = pluginInstance;
                        } else if (defaults.debug) {

                            console.log('The ' + methodName + ' method of the ' + pluginName
                                    + ' plugin conflicts with another plugin or native methods');
                        }
                    }
                });

                obj[pluginName] = pluginInstance;
            }

            return this;
        },

        /**
         * For mockup in Node env if need be, for testing purposes
         *
         * @return {core}
         * @private
         */
        __setWindow: function (window) {
            env.window = window;
            return this;
        },

        /**
         * Returns a ruler, a tool to help measure the size of a tooltip under
         * various settings. Meant for plugins
         * 
         * @see Ruler
         * @return {object} A Ruler instance
         * @protected
         */
        _getRuler: function ($tooltip) {
            return new Ruler($tooltip);
        },

        /**
         * For internal use by plugins, if needed
         *
         * @return {core}
         * @protected
         */
        _off: function () {
            this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
            return this;
        },

        /**
         * For internal use by plugins, if needed
         *
         * @return {core}
         * @protected
         */
        _on: function () {
            this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
            return this;
        },

        /**
         * For internal use by plugins, if needed
         *
         * @return {core}
         * @protected
         */
        _one: function () {
            this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
            return this;
        },

        /**
         * Returns (getter) or adds (setter) a plugin
         *
         * @param {string|object} plugin Provide a string (in the full form
         * "namespace.name") to use as as getter, an object to use as a setter
         * @return {object|core}
         * @protected
         */
        _plugin: function (plugin) {

            var self = this;

            // getter
            if (typeof plugin == 'string') {

                var pluginName = plugin,
                        p = null;

                // if the namespace is provided, it's easy to search
                if (pluginName.indexOf('.') > 0) {
                    p = self.__plugins[pluginName];
                }
                // otherwise, return the first name that matches
                else {
                    $.each(self.__plugins, function (i, plugin) {

                        if (plugin.name.substring(plugin.name.length - pluginName.length - 1) == '.' + pluginName) {
                            p = plugin;
                            return false;
                        }
                    });
                }

                return p;
            }
            // setter
            else {

                // force namespaces
                if (plugin.name.indexOf('.') < 0) {
                    throw new Error('Plugins must be namespaced');
                }

                self.__plugins[plugin.name] = plugin;

                // if the plugin has core features
                if (plugin.core) {

                    // bridge non-private methods onto the core to allow new core methods
                    self.__bridge(plugin.core, self, plugin.name);
                }

                return this;
            }
        },

        /**
         * Trigger events on the core emitters
         * 
         * @returns {core}
         * @protected
         */
        _trigger: function () {

            var args = Array.prototype.slice.apply(arguments);

            if (typeof args[0] == 'string') {
                args[0] = {type: args[0]};
            }

            // note: the order of emitters matters
            this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
            this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);

            return this;
        },

        /**
         * Returns instances of all tooltips in the page or an a given element
         *
         * @param {string|HTML object collection} selector optional Use this
         * parameter to restrict the set of objects that will be inspected
         * for the retrieval of instances. By default, all instances in the
         * page are returned.
         * @return {array} An array of instance objects
         * @public
         */
        instances: function (selector) {

            var instances = [],
                    sel = selector || '.tooltipstered';

            $(sel).each(function () {

                var $this = $(this),
                        ns = $this.data('tooltipster-ns');

                if (ns) {

                    $.each(ns, function (i, namespace) {
                        instances.push($this.data(namespace));
                    });
                }
            });

            return instances;
        },

        /**
         * Returns the Tooltipster objects generated by the last initializing call
         *
         * @return {array} An array of instance objects
         * @public
         */
        instancesLatest: function () {
            return this.__instancesLatestArr;
        },

        /**
         * For public use only, not to be used by plugins (use ::_off() instead)
         *
         * @return {core}
         * @public
         */
        off: function () {
            this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
            return this;
        },

        /**
         * For public use only, not to be used by plugins (use ::_on() instead)
         *
         * @return {core}
         * @public
         */
        on: function () {
            this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
            return this;
        },

        /**
         * For public use only, not to be used by plugins (use ::_one() instead)
         * 
         * @return {core}
         * @public
         */
        one: function () {
            this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
            return this;
        },

        /**
         * Returns all HTML elements which have one or more tooltips
         *
         * @param {string} selector optional Use this to restrict the results
         * to the descendants of an element
         * @return {array} An array of HTML elements
         * @public
         */
        origins: function (selector) {

            var sel = selector ?
                    selector + ' ' :
                    '';

            return $(sel + '.tooltipstered').toArray();
        },

        /**
         * Change default options for all future instances
         *
         * @param {object} d The options that should be made defaults
         * @return {core}
         * @public
         */
        setDefaults: function (d) {
            $.extend(defaults, d);
            return this;
        },

        /**
         * For users to trigger their handlers on the public emitter
         * 
         * @returns {core}
         * @public
         */
        triggerHandler: function () {
            this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
            return this;
        }
    };

// $.tooltipster will be used to call core methods
    $.tooltipster = new core();

// the Tooltipster instance class (mind the capital T)
    $.Tooltipster = function (element, options) {

        // list of instance variables

        // stack of custom callbacks provided as parameters to API methods
        this.__callbacks = {
            close: [],
            open: []
        };
        // the schedule time of DOM removal
        this.__closingTime;
        // this will be the user content shown in the tooltip. A capital "C" is used
        // because there is also a method called content()
        this.__Content;
        // for the size tracker
        this.__contentBcr;
        // to disable the tooltip after destruction
        this.__destroyed = false;
        // we can't emit directly on the instance because if a method with the same
        // name as the event exists, it will be called by jQuery. Se we use a plain
        // object as emitter. This emitter is for internal use by plugins,
        // if needed.
        this.__$emitterPrivate = $({});
        // this emitter is for the user to listen to events without risking to mess
        // with our internal listeners
        this.__$emitterPublic = $({});
        this.__enabled = true;
        // the reference to the gc interval
        this.__garbageCollector;
        // various position and size data recomputed before each repositioning
        this.__Geometry;
        // the tooltip position, saved after each repositioning by a plugin
        this.__lastPosition;
        // a unique namespace per instance
        this.__namespace = 'tooltipster-' + Math.round(Math.random() * 1000000);
        this.__options;
        // will be used to support origins in scrollable areas
        this.__$originParents;
        this.__pointerIsOverOrigin = false;
        // to remove themes if needed
        this.__previousThemes = [];
        // the state can be either: appearing, stable, disappearing, closed
        this.__state = 'closed';
        // timeout references
        this.__timeouts = {
            close: [],
            open: null
        };
        // store touch events to be able to detect emulated mouse events
        this.__touchEvents = [];
        // the reference to the tracker interval
        this.__tracker = null;
        // the element to which this tooltip is associated
        this._$origin;
        // this will be the tooltip element (jQuery wrapped HTML element).
        // It's the job of a plugin to create it and append it to the DOM
        this._$tooltip;

        // launch
        this.__init(element, options);
    };

    $.Tooltipster.prototype = {

        /**
         * @param origin
         * @param options
         * @private
         */
        __init: function (origin, options) {

            var self = this;

            self._$origin = $(origin);
            self.__options = $.extend(true, {}, defaults, options);

            // some options may need to be reformatted
            self.__optionsFormat();

            // don't run on old IE if asked no to
            if (!env.IE
                    || env.IE >= self.__options.IEmin
                    ) {

                // note: the content is null (empty) by default and can stay that
                // way if the plugin remains initialized but not fed any content. The
                // tooltip will just not appear.

                // let's save the initial value of the title attribute for later
                // restoration if need be.
                var initialTitle = null;

                // it will already have been saved in case of multiple tooltips
                if (self._$origin.data('tooltipster-initialTitle') === undefined) {

                    initialTitle = self._$origin.attr('title');

                    // we do not want initialTitle to be "undefined" because
                    // of how jQuery's .data() method works
                    if (initialTitle === undefined)
                        initialTitle = null;

                    self._$origin.data('tooltipster-initialTitle', initialTitle);
                }

                // If content is provided in the options, it has precedence over the
                // title attribute.
                // Note: an empty string is considered content, only 'null' represents
                // the absence of content.
                // Also, an existing title="" attribute will result in an empty string
                // content
                if (self.__options.content !== null) {
                    self.__contentSet(self.__options.content);
                } else {

                    var selector = self._$origin.attr('data-tooltip-content'),
                            $el;

                    if (selector) {
                        $el = $(selector);
                    }

                    if ($el && $el[0]) {
                        self.__contentSet($el.first());
                    } else {
                        self.__contentSet(initialTitle);
                    }
                }

                self._$origin
                        // strip the title off of the element to prevent the default tooltips
                        // from popping up
                        .removeAttr('title')
                        // to be able to find all instances on the page later (upon window
                        // events in particular)
                        .addClass('tooltipstered');

                // set listeners on the origin
                self.__prepareOrigin();

                // set the garbage collector
                self.__prepareGC();

                // init plugins
                $.each(self.__options.plugins, function (i, pluginName) {
                    self._plug(pluginName);
                });

                // to detect swiping
                if (env.hasTouchCapability) {
                    $(env.window.document.body).on('touchmove.' + self.__namespace + '-triggerOpen', function (event) {
                        self._touchRecordEvent(event);
                    });
                }

                self
                        // prepare the tooltip when it gets created. This event must
                        // be fired by a plugin
                        ._on('created', function () {
                            self.__prepareTooltip();
                        })
                        // save position information when it's sent by a plugin
                        ._on('repositioned', function (e) {
                            self.__lastPosition = e.position;
                        });
            } else {
                self.__options.disabled = true;
            }
        },

        /**
         * Insert the content into the appropriate HTML element of the tooltip
         * 
         * @returns {self}
         * @private
         */
        __contentInsert: function () {

            var self = this,
                    $el = self._$tooltip.find('.tooltipster-content'),
                    formattedContent = self.__Content,
                    format = function (content) {
                        formattedContent = content;
                    };

            self._trigger({
                type: 'format',
                content: self.__Content,
                format: format
            });

            if (self.__options.functionFormat) {

                formattedContent = self.__options.functionFormat.call(
                        self,
                        self,
                        {origin: self._$origin[0]},
                        self.__Content
                        );
            }

            if (typeof formattedContent === 'string' && !self.__options.contentAsHTML) {
                $el.text(formattedContent);
            } else {
                $el
                        .empty()
                        .append(formattedContent);
            }

            return self;
        },

        /**
         * Save the content, cloning it beforehand if need be
         * 
         * @param content
         * @returns {self}
         * @private
         */
        __contentSet: function (content) {

            // clone if asked. Cloning the object makes sure that each instance has its
            // own version of the content (in case a same object were provided for several
            // instances)
            // reminder: typeof null === object
            if (content instanceof $ && this.__options.contentCloning) {
                content = content.clone(true);
            }

            this.__Content = content;

            this._trigger({
                type: 'updated',
                content: content
            });

            return this;
        },

        /**
         * Error message about a method call made after destruction
         * 
         * @private
         */
        __destroyError: function () {
            throw new Error('This tooltip has been destroyed and cannot execute your method call.');
        },

        /**
         * Gather all information about dimensions and available space,
         * called before every repositioning
         * 
         * @private
         * @returns {object}
         */
        __geometry: function () {

            var self = this,
                    $target = self._$origin,
                    originIsArea = self._$origin.is('area');

            // if this._$origin is a map area, the target we'll need
            // the dimensions of is actually the image using the map,
            // not the area itself
            if (originIsArea) {

                var mapName = self._$origin.parent().attr('name');

                $target = $('img[usemap="#' + mapName + '"]');
            }

            var bcr = $target[0].getBoundingClientRect(),
                    $document = $(env.window.document),
                    $window = $(env.window),
                    $parent = $target,
                    // some useful properties of important elements
                    geo = {
                        // available space for the tooltip, see down below
                        available: {
                            document: null,
                            window: null
                        },
                        document: {
                            size: {
                                height: $document.height(),
                                width: $document.width()
                            }
                        },
                        window: {
                            scroll: {
                                // the second ones are for IE compatibility
                                left: env.window.scrollX || env.window.document.documentElement.scrollLeft,
                                top: env.window.scrollY || env.window.document.documentElement.scrollTop
                            },
                            size: {
                                height: $window.height(),
                                width: $window.width()
                            }
                        },
                        origin: {
                            // the origin has a fixed lineage if itself or one of its
                            // ancestors has a fixed position
                            fixedLineage: false,
                            // relative to the document
                            offset: {},
                            size: {
                                height: bcr.bottom - bcr.top,
                                width: bcr.right - bcr.left
                            },
                            usemapImage: originIsArea ? $target[0] : null,
                            // relative to the window
                            windowOffset: {
                                bottom: bcr.bottom,
                                left: bcr.left,
                                right: bcr.right,
                                top: bcr.top
                            }
                        }
                    },
                    geoFixed = false;

            // if the element is a map area, some properties may need
            // to be recalculated
            if (originIsArea) {

                var shape = self._$origin.attr('shape'),
                        coords = self._$origin.attr('coords');

                if (coords) {

                    coords = coords.split(',');

                    $.map(coords, function (val, i) {
                        coords[i] = parseInt(val);
                    });
                }

                // if the image itself is the area, nothing more to do
                if (shape != 'default') {

                    switch (shape) {

                        case 'circle':

                            var circleCenterLeft = coords[0],
                                    circleCenterTop = coords[1],
                                    circleRadius = coords[2],
                                    areaTopOffset = circleCenterTop - circleRadius,
                                    areaLeftOffset = circleCenterLeft - circleRadius;

                            geo.origin.size.height = circleRadius * 2;
                            geo.origin.size.width = geo.origin.size.height;

                            geo.origin.windowOffset.left += areaLeftOffset;
                            geo.origin.windowOffset.top += areaTopOffset;

                            break;

                        case 'rect':

                            var areaLeft = coords[0],
                                    areaTop = coords[1],
                                    areaRight = coords[2],
                                    areaBottom = coords[3];

                            geo.origin.size.height = areaBottom - areaTop;
                            geo.origin.size.width = areaRight - areaLeft;

                            geo.origin.windowOffset.left += areaLeft;
                            geo.origin.windowOffset.top += areaTop;

                            break;

                        case 'poly':

                            var areaSmallestX = 0,
                                    areaSmallestY = 0,
                                    areaGreatestX = 0,
                                    areaGreatestY = 0,
                                    arrayAlternate = 'even';

                            for (var i = 0; i < coords.length; i++) {

                                var areaNumber = coords[i];

                                if (arrayAlternate == 'even') {

                                    if (areaNumber > areaGreatestX) {

                                        areaGreatestX = areaNumber;

                                        if (i === 0) {
                                            areaSmallestX = areaGreatestX;
                                        }
                                    }

                                    if (areaNumber < areaSmallestX) {
                                        areaSmallestX = areaNumber;
                                    }

                                    arrayAlternate = 'odd';
                                } else {
                                    if (areaNumber > areaGreatestY) {

                                        areaGreatestY = areaNumber;

                                        if (i == 1) {
                                            areaSmallestY = areaGreatestY;
                                        }
                                    }

                                    if (areaNumber < areaSmallestY) {
                                        areaSmallestY = areaNumber;
                                    }

                                    arrayAlternate = 'even';
                                }
                            }

                            geo.origin.size.height = areaGreatestY - areaSmallestY;
                            geo.origin.size.width = areaGreatestX - areaSmallestX;

                            geo.origin.windowOffset.left += areaSmallestX;
                            geo.origin.windowOffset.top += areaSmallestY;

                            break;
                    }
                }
            }

            // user callback through an event
            var edit = function (r) {
                geo.origin.size.height = r.height,
                        geo.origin.windowOffset.left = r.left,
                        geo.origin.windowOffset.top = r.top,
                        geo.origin.size.width = r.width
            };

            self._trigger({
                type: 'geometry',
                edit: edit,
                geometry: {
                    height: geo.origin.size.height,
                    left: geo.origin.windowOffset.left,
                    top: geo.origin.windowOffset.top,
                    width: geo.origin.size.width
                }
            });

            // calculate the remaining properties with what we got

            geo.origin.windowOffset.right = geo.origin.windowOffset.left + geo.origin.size.width;
            geo.origin.windowOffset.bottom = geo.origin.windowOffset.top + geo.origin.size.height;

            geo.origin.offset.left = geo.origin.windowOffset.left + geo.window.scroll.left;
            geo.origin.offset.top = geo.origin.windowOffset.top + geo.window.scroll.top;
            geo.origin.offset.bottom = geo.origin.offset.top + geo.origin.size.height;
            geo.origin.offset.right = geo.origin.offset.left + geo.origin.size.width;

            // the space that is available to display the tooltip relatively to the document
            geo.available.document = {
                bottom: {
                    height: geo.document.size.height - geo.origin.offset.bottom,
                    width: geo.document.size.width
                },
                left: {
                    height: geo.document.size.height,
                    width: geo.origin.offset.left
                },
                right: {
                    height: geo.document.size.height,
                    width: geo.document.size.width - geo.origin.offset.right
                },
                top: {
                    height: geo.origin.offset.top,
                    width: geo.document.size.width
                }
            };

            // the space that is available to display the tooltip relatively to the viewport
            // (the resulting values may be negative if the origin overflows the viewport)
            geo.available.window = {
                bottom: {
                    // the inner max is here to make sure the available height is no bigger
                    // than the viewport height (when the origin is off screen at the top).
                    // The outer max just makes sure that the height is not negative (when
                    // the origin overflows at the bottom).
                    height: Math.max(geo.window.size.height - Math.max(geo.origin.windowOffset.bottom, 0), 0),
                    width: geo.window.size.width
                },
                left: {
                    height: geo.window.size.height,
                    width: Math.max(geo.origin.windowOffset.left, 0)
                },
                right: {
                    height: geo.window.size.height,
                    width: Math.max(geo.window.size.width - Math.max(geo.origin.windowOffset.right, 0), 0)
                },
                top: {
                    height: Math.max(geo.origin.windowOffset.top, 0),
                    width: geo.window.size.width
                }
            };

            while ($parent[0].tagName.toLowerCase() != 'html') {

                if ($parent.css('position') == 'fixed') {
                    geo.origin.fixedLineage = true;
                    break;
                }

                $parent = $parent.parent();
            }

            return geo;
        },

        /**
         * Some options may need to be formated before being used
         * 
         * @returns {self}
         * @private
         */
        __optionsFormat: function () {

            if (typeof this.__options.animationDuration == 'number') {
                this.__options.animationDuration = [this.__options.animationDuration, this.__options.animationDuration];
            }

            if (typeof this.__options.delay == 'number') {
                this.__options.delay = [this.__options.delay, this.__options.delay];
            }

            if (typeof this.__options.delayTouch == 'number') {
                this.__options.delayTouch = [this.__options.delayTouch, this.__options.delayTouch];
            }

            if (typeof this.__options.theme == 'string') {
                this.__options.theme = [this.__options.theme];
            }

            // determine the future parent
            if (this.__options.parent === null) {
                this.__options.parent = $(env.window.document.body);
            } else if (typeof this.__options.parent == 'string') {
                this.__options.parent = $(this.__options.parent);
            }

            if (this.__options.trigger == 'hover') {

                this.__options.triggerOpen = {
                    mouseenter: true,
                    touchstart: true
                };

                this.__options.triggerClose = {
                    mouseleave: true,
                    originClick: true,
                    touchleave: true
                };
            } else if (this.__options.trigger == 'click') {

                this.__options.triggerOpen = {
                    click: true,
                    tap: true
                };

                this.__options.triggerClose = {
                    click: true,
                    tap: true
                };
            }

            // for the plugins
            this._trigger('options');

            return this;
        },

        /**
         * Schedules or cancels the garbage collector task
         *
         * @returns {self}
         * @private
         */
        __prepareGC: function () {

            var self = this;

            // in case the selfDestruction option has been changed by a method call
            if (self.__options.selfDestruction) {

                // the GC task
                self.__garbageCollector = setInterval(function () {

                    var now = new Date().getTime();

                    // forget the old events
                    self.__touchEvents = $.grep(self.__touchEvents, function (event, i) {
                        // 1 minute
                        return now - event.time > 60000;
                    });

                    // auto-destruct if the origin is gone
                    if (!bodyContains(self._$origin)) {

                        self.close(function () {
                            self.destroy();
                        });
                    }
                }, 20000);
            } else {
                clearInterval(self.__garbageCollector);
            }

            return self;
        },

        /**
         * Sets listeners on the origin if the open triggers require them.
         * Unlike the listeners set at opening time, these ones
         * remain even when the tooltip is closed. It has been made a
         * separate method so it can be called when the triggers are
         * changed in the options. Closing is handled in _open()
         * because of the bindings that may be needed on the tooltip
         * itself
         *
         * @returns {self}
         * @private
         */
        __prepareOrigin: function () {

            var self = this;

            // in case we're resetting the triggers
            self._$origin.off('.' + self.__namespace + '-triggerOpen');

            // if the device is touch capable, even if only mouse triggers
            // are asked, we need to listen to touch events to know if the mouse
            // events are actually emulated (so we can ignore them)
            if (env.hasTouchCapability) {

                self._$origin.on(
                        'touchstart.' + self.__namespace + '-triggerOpen ' +
                        'touchend.' + self.__namespace + '-triggerOpen ' +
                        'touchcancel.' + self.__namespace + '-triggerOpen',
                        function (event) {
                            self._touchRecordEvent(event);
                        }
                );
            }

            // mouse click and touch tap work the same way
            if (self.__options.triggerOpen.click
                    || (self.__options.triggerOpen.tap && env.hasTouchCapability)
                    ) {

                var eventNames = '';
                if (self.__options.triggerOpen.click) {
                    eventNames += 'click.' + self.__namespace + '-triggerOpen ';
                }
                if (self.__options.triggerOpen.tap && env.hasTouchCapability) {
                    eventNames += 'touchend.' + self.__namespace + '-triggerOpen';
                }

                self._$origin.on(eventNames, function (event) {
                    if (self._touchIsMeaningfulEvent(event)) {
                        self._open(event);
                    }
                });
            }

            // mouseenter and touch start work the same way
            if (self.__options.triggerOpen.mouseenter
                    || (self.__options.triggerOpen.touchstart && env.hasTouchCapability)
                    ) {

                var eventNames = '';
                if (self.__options.triggerOpen.mouseenter) {
                    eventNames += 'mouseenter.' + self.__namespace + '-triggerOpen ';
                }
                if (self.__options.triggerOpen.touchstart && env.hasTouchCapability) {
                    eventNames += 'touchstart.' + self.__namespace + '-triggerOpen';
                }

                self._$origin.on(eventNames, function (event) {
                    if (self._touchIsTouchEvent(event)
                            || !self._touchIsEmulatedEvent(event)
                            ) {
                        self.__pointerIsOverOrigin = true;
                        self._openShortly(event);
                    }
                });
            }

            // info for the mouseleave/touchleave close triggers when they use a delay
            if (self.__options.triggerClose.mouseleave
                    || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
                    ) {

                var eventNames = '';
                if (self.__options.triggerClose.mouseleave) {
                    eventNames += 'mouseleave.' + self.__namespace + '-triggerOpen ';
                }
                if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
                    eventNames += 'touchend.' + self.__namespace + '-triggerOpen touchcancel.' + self.__namespace + '-triggerOpen';
                }

                self._$origin.on(eventNames, function (event) {

                    if (self._touchIsMeaningfulEvent(event)) {
                        self.__pointerIsOverOrigin = false;
                    }
                });
            }

            return self;
        },

        /**
         * Do the things that need to be done only once after the tooltip
         * HTML element it has been created. It has been made a separate
         * method so it can be called when options are changed. Remember
         * that the tooltip may actually exist in the DOM before it is
         * opened, and present after it has been closed: it's the display
         * plugin that takes care of handling it.
         * 
         * @returns {self}
         * @private
         */
        __prepareTooltip: function () {

            var self = this,
                    p = self.__options.interactive ? 'auto' : '';

            // this will be useful to know quickly if the tooltip is in
            // the DOM or not 
            self._$tooltip
                    .attr('id', self.__namespace)
                    .css({
                        // pointer events
                        'pointer-events': p,
                        zIndex: self.__options.zIndex
                    });

            // themes
            // remove the old ones and add the new ones
            $.each(self.__previousThemes, function (i, theme) {
                self._$tooltip.removeClass(theme);
            });
            $.each(self.__options.theme, function (i, theme) {
                self._$tooltip.addClass(theme);
            });

            self.__previousThemes = $.merge([], self.__options.theme);

            return self;
        },

        /**
         * Handles the scroll on any of the parents of the origin (when the
         * tooltip is open)
         *
         * @param {object} event
         * @returns {self}
         * @private
         */
        __scrollHandler: function (event) {

            var self = this;

            if (self.__options.triggerClose.scroll) {
                self._close(event);
            } else {

                // if the origin or tooltip have been removed: do nothing, the tracker will
                // take care of it later
                if (bodyContains(self._$origin) && bodyContains(self._$tooltip)) {

                    var geo = null;

                    // if the scroll happened on the window
                    if (event.target === env.window.document) {

                        // if the origin has a fixed lineage, window scroll will have no
                        // effect on its position nor on the position of the tooltip
                        if (!self.__Geometry.origin.fixedLineage) {

                            // we don't need to do anything unless repositionOnScroll is true
                            // because the tooltip will already have moved with the window
                            // (and of course with the origin)
                            if (self.__options.repositionOnScroll) {
                                self.reposition(event);
                            }
                        }
                    }
                    // if the scroll happened on another parent of the tooltip, it means
                    // that it's in a scrollable area and now needs to have its position
                    // adjusted or recomputed, depending ont the repositionOnScroll
                    // option. Also, if the origin is partly hidden due to a parent that
                    // hides its overflow, we'll just hide (not close) the tooltip.
                    else {

                        geo = self.__geometry();

                        var overflows = false;

                        // a fixed position origin is not affected by the overflow hiding
                        // of a parent
                        if (self._$origin.css('position') != 'fixed') {

                            self.__$originParents.each(function (i, el) {

                                var $el = $(el),
                                        overflowX = $el.css('overflow-x'),
                                        overflowY = $el.css('overflow-y');

                                if (overflowX != 'visible' || overflowY != 'visible') {

                                    var bcr = el.getBoundingClientRect();

                                    if (overflowX != 'visible') {

                                        if (geo.origin.windowOffset.left < bcr.left
                                                || geo.origin.windowOffset.right > bcr.right
                                                ) {
                                            overflows = true;
                                            return false;
                                        }
                                    }

                                    if (overflowY != 'visible') {

                                        if (geo.origin.windowOffset.top < bcr.top
                                                || geo.origin.windowOffset.bottom > bcr.bottom
                                                ) {
                                            overflows = true;
                                            return false;
                                        }
                                    }
                                }

                                // no need to go further if fixed, for the same reason as above
                                if ($el.css('position') == 'fixed') {
                                    return false;
                                }
                            });
                        }

                        if (overflows) {
                            self._$tooltip.css('visibility', 'hidden');
                        } else {

                            self._$tooltip.css('visibility', 'visible');

                            // reposition
                            if (self.__options.repositionOnScroll) {
                                self.reposition(event);
                            }
                            // or just adjust offset
                            else {

                                // we have to use offset and not windowOffset because this way,
                                // only the scroll distance of the scrollable areas are taken into
                                // account (the scrolltop value of the main window must be
                                // ignored since the tooltip already moves with it)
                                var offsetLeft = geo.origin.offset.left - self.__Geometry.origin.offset.left,
                                        offsetTop = geo.origin.offset.top - self.__Geometry.origin.offset.top;

                                // add the offset to the position initially computed by the display plugin
                                self._$tooltip.css({
                                    left: self.__lastPosition.coord.left + offsetLeft,
                                    top: self.__lastPosition.coord.top + offsetTop
                                });
                            }
                        }
                    }

                    self._trigger({
                        type: 'scroll',
                        event: event,
                        geo: geo
                    });
                }
            }

            return self;
        },

        /**
         * Changes the state of the tooltip
         *
         * @param {string} state
         * @returns {self}
         * @private
         */
        __stateSet: function (state) {

            this.__state = state;

            this._trigger({
                type: 'state',
                state: state
            });

            return this;
        },

        /**
         * Clear appearance timeouts
         *
         * @returns {self}
         * @private
         */
        __timeoutsClear: function () {

            // there is only one possible open timeout: the delayed opening
            // when the mouseenter/touchstart open triggers are used
            clearTimeout(this.__timeouts.open);
            this.__timeouts.open = null;

            // ... but several close timeouts: the delayed closing when the
            // mouseleave close trigger is used and the timer option
            $.each(this.__timeouts.close, function (i, timeout) {
                clearTimeout(timeout);
            });
            this.__timeouts.close = [];

            return this;
        },

        /**
         * Start the tracker that will make checks at regular intervals
         * 
         * @returns {self}
         * @private
         */
        __trackerStart: function () {

            var self = this,
                    $content = self._$tooltip.find('.tooltipster-content');

            // get the initial content size
            if (self.__options.trackTooltip) {
                self.__contentBcr = $content[0].getBoundingClientRect();
            }

            self.__tracker = setInterval(function () {

                // if the origin or tooltip elements have been removed.
                // Note: we could destroy the instance now if the origin has
                // been removed but we'll leave that task to our garbage collector
                if (!bodyContains(self._$origin) || !bodyContains(self._$tooltip)) {
                    self._close();
                }
                // if everything is alright
                else {

                    // compare the former and current positions of the origin to reposition
                    // the tooltip if need be
                    if (self.__options.trackOrigin) {

                        var g = self.__geometry(),
                                identical = false;

                        // compare size first (a change requires repositioning too)
                        if (areEqual(g.origin.size, self.__Geometry.origin.size)) {

                            // for elements that have a fixed lineage (see __geometry()), we track the
                            // top and left properties (relative to window)
                            if (self.__Geometry.origin.fixedLineage) {
                                if (areEqual(g.origin.windowOffset, self.__Geometry.origin.windowOffset)) {
                                    identical = true;
                                }
                            }
                            // otherwise, track total offset (relative to document)
                            else {
                                if (areEqual(g.origin.offset, self.__Geometry.origin.offset)) {
                                    identical = true;
                                }
                            }
                        }

                        if (!identical) {

                            // close the tooltip when using the mouseleave close trigger
                            // (see https://github.com/iamceege/tooltipster/pull/253)
                            if (self.__options.triggerClose.mouseleave) {
                                self._close();
                            } else {
                                self.reposition();
                            }
                        }
                    }

                    if (self.__options.trackTooltip) {

                        var currentBcr = $content[0].getBoundingClientRect();

                        if (currentBcr.height !== self.__contentBcr.height
                                || currentBcr.width !== self.__contentBcr.width
                                ) {
                            self.reposition();
                            self.__contentBcr = currentBcr;
                        }
                    }
                }
            }, self.__options.trackerInterval);

            return self;
        },

        /**
         * Closes the tooltip (after the closing delay)
         * 
         * @param event
         * @param callback
         * @param force Set to true to override a potential refusal of the user's function
         * @returns {self}
         * @protected
         */
        _close: function (event, callback, force) {

            var self = this,
                    ok = true;

            self._trigger({
                type: 'close',
                event: event,
                stop: function () {
                    ok = false;
                }
            });

            // a destroying tooltip (force == true) may not refuse to close
            if (ok || force) {

                // save the method custom callback and cancel any open method custom callbacks
                if (callback)
                    self.__callbacks.close.push(callback);
                self.__callbacks.open = [];

                // clear open/close timeouts
                self.__timeoutsClear();

                var finishCallbacks = function () {

                    // trigger any close method custom callbacks and reset them
                    $.each(self.__callbacks.close, function (i, c) {
                        c.call(self, self, {
                            event: event,
                            origin: self._$origin[0]
                        });
                    });

                    self.__callbacks.close = [];
                };

                if (self.__state != 'closed') {

                    var necessary = true,
                            d = new Date(),
                            now = d.getTime(),
                            newClosingTime = now + self.__options.animationDuration[1];

                    // the tooltip may already already be disappearing, but if a new
                    // call to close() is made after the animationDuration was changed
                    // to 0 (for example), we ought to actually close it sooner than
                    // previously scheduled. In that case it should be noted that the
                    // browser will not adapt the animation duration to the new
                    // animationDuration that was set after the start of the closing
                    // animation.
                    // Note: the same thing could be considered at opening, but is not
                    // really useful since the tooltip is actually opened immediately
                    // upon a call to _open(). Since it would not make the opening
                    // animation finish sooner, its sole impact would be to trigger the
                    // state event and the open callbacks sooner than the actual end of
                    // the opening animation, which is not great.
                    if (self.__state == 'disappearing') {

                        if (newClosingTime > self.__closingTime
                                // in case closing is actually overdue because the script
                                // execution was suspended. See #679
                                && self.__options.animationDuration[1] > 0
                                ) {
                            necessary = false;
                        }
                    }

                    if (necessary) {

                        self.__closingTime = newClosingTime;

                        if (self.__state != 'disappearing') {
                            self.__stateSet('disappearing');
                        }

                        var finish = function () {

                            // stop the tracker
                            clearInterval(self.__tracker);

                            // a "beforeClose" option has been asked several times but would
                            // probably useless since the content element is still accessible
                            // via ::content(), and because people can always use listeners
                            // inside their content to track what's going on. For the sake of
                            // simplicity, this has been denied. Bur for the rare people who
                            // really need the option (for old browsers or for the case where
                            // detaching the content is actually destructive, for file or
                            // password inputs for example), this event will do the work.
                            self._trigger({
                                type: 'closing',
                                event: event
                            });

                            // unbind listeners which are no longer needed

                            self._$tooltip
                                    .off('.' + self.__namespace + '-triggerClose')
                                    .removeClass('tooltipster-dying');

                            // orientationchange, scroll and resize listeners
                            $(env.window).off('.' + self.__namespace + '-triggerClose');

                            // scroll listeners
                            self.__$originParents.each(function (i, el) {
                                $(el).off('scroll.' + self.__namespace + '-triggerClose');
                            });
                            // clear the array to prevent memory leaks
                            self.__$originParents = null;

                            $(env.window.document.body).off('.' + self.__namespace + '-triggerClose');

                            self._$origin.off('.' + self.__namespace + '-triggerClose');

                            self._off('dismissable');

                            // a plugin that would like to remove the tooltip from the
                            // DOM when closed should bind on this
                            self.__stateSet('closed');

                            // trigger event
                            self._trigger({
                                type: 'after',
                                event: event
                            });

                            // call our constructor custom callback function
                            if (self.__options.functionAfter) {
                                self.__options.functionAfter.call(self, self, {
                                    event: event,
                                    origin: self._$origin[0]
                                });
                            }

                            // call our method custom callbacks functions
                            finishCallbacks();
                        };

                        if (env.hasTransitions) {

                            self._$tooltip.css({
                                '-moz-animation-duration': self.__options.animationDuration[1] + 'ms',
                                '-ms-animation-duration': self.__options.animationDuration[1] + 'ms',
                                '-o-animation-duration': self.__options.animationDuration[1] + 'ms',
                                '-webkit-animation-duration': self.__options.animationDuration[1] + 'ms',
                                'animation-duration': self.__options.animationDuration[1] + 'ms',
                                'transition-duration': self.__options.animationDuration[1] + 'ms'
                            });

                            self._$tooltip
                                    // clear both potential open and close tasks
                                    .clearQueue()
                                    .removeClass('tooltipster-show')
                                    // for transitions only
                                    .addClass('tooltipster-dying');

                            if (self.__options.animationDuration[1] > 0) {
                                self._$tooltip.delay(self.__options.animationDuration[1]);
                            }

                            self._$tooltip.queue(finish);
                        } else {

                            self._$tooltip
                                    .stop()
                                    .fadeOut(self.__options.animationDuration[1], finish);
                        }
                    }
                }
                // if the tooltip is already closed, we still need to trigger
                // the method custom callbacks
                else {
                    finishCallbacks();
                }
            }

            return self;
        },

        /**
         * For internal use by plugins, if needed
         * 
         * @returns {self}
         * @protected
         */
        _off: function () {
            this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
            return this;
        },

        /**
         * For internal use by plugins, if needed
         *
         * @returns {self}
         * @protected
         */
        _on: function () {
            this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
            return this;
        },

        /**
         * For internal use by plugins, if needed
         *
         * @returns {self}
         * @protected
         */
        _one: function () {
            this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
            return this;
        },

        /**
         * Opens the tooltip right away.
         *
         * @param event
         * @param callback Will be called when the opening animation is over
         * @returns {self}
         * @protected
         */
        _open: function (event, callback) {

            var self = this;

            // if the destruction process has not begun and if this was not
            // triggered by an unwanted emulated click event
            if (!self.__destroying) {

                // check that the origin is still in the DOM
                if (bodyContains(self._$origin)
                        // if the tooltip is enabled
                        && self.__enabled
                        ) {

                    var ok = true;

                    // if the tooltip is not open yet, we need to call functionBefore.
                    // otherwise we can jst go on
                    if (self.__state == 'closed') {

                        // trigger an event. The event.stop function allows the callback
                        // to prevent the opening of the tooltip
                        self._trigger({
                            type: 'before',
                            event: event,
                            stop: function () {
                                ok = false;
                            }
                        });

                        if (ok && self.__options.functionBefore) {

                            // call our custom function before continuing
                            ok = self.__options.functionBefore.call(self, self, {
                                event: event,
                                origin: self._$origin[0]
                            });
                        }
                    }

                    if (ok !== false) {

                        // if there is some content
                        if (self.__Content !== null) {

                            // save the method callback and cancel close method callbacks
                            if (callback) {
                                self.__callbacks.open.push(callback);
                            }
                            self.__callbacks.close = [];

                            // get rid of any appearance timeouts
                            self.__timeoutsClear();

                            var extraTime,
                                    finish = function () {

                                        if (self.__state != 'stable') {
                                            self.__stateSet('stable');
                                        }

                                        // trigger any open method custom callbacks and reset them
                                        $.each(self.__callbacks.open, function (i, c) {
                                            c.call(self, self, {
                                                origin: self._$origin[0],
                                                tooltip: self._$tooltip[0]
                                            });
                                        });

                                        self.__callbacks.open = [];
                                    };

                            // if the tooltip is already open
                            if (self.__state !== 'closed') {

                                // the timer (if any) will start (or restart) right now
                                extraTime = 0;

                                // if it was disappearing, cancel that
                                if (self.__state === 'disappearing') {

                                    self.__stateSet('appearing');

                                    if (env.hasTransitions) {

                                        self._$tooltip
                                                .clearQueue()
                                                .removeClass('tooltipster-dying')
                                                .addClass('tooltipster-show');

                                        if (self.__options.animationDuration[0] > 0) {
                                            self._$tooltip.delay(self.__options.animationDuration[0]);
                                        }

                                        self._$tooltip.queue(finish);
                                    } else {
                                        // in case the tooltip was currently fading out, bring it back
                                        // to life
                                        self._$tooltip
                                                .stop()
                                                .fadeIn(finish);
                                    }
                                }
                                // if the tooltip is already open, we still need to trigger the method
                                // custom callback
                                else if (self.__state == 'stable') {
                                    finish();
                                }
                            }
                            // if the tooltip isn't already open, open it
                            else {

                                // a plugin must bind on this and store the tooltip in this._$tooltip
                                self.__stateSet('appearing');

                                // the timer (if any) will start when the tooltip has fully appeared
                                // after its transition
                                extraTime = self.__options.animationDuration[0];

                                // insert the content inside the tooltip
                                self.__contentInsert();

                                // reposition the tooltip and attach to the DOM
                                self.reposition(event, true);

                                // animate in the tooltip. If the display plugin wants no css
                                // animations, it may override the animation option with a
                                // dummy value that will produce no effect
                                if (env.hasTransitions) {

                                    // note: there seems to be an issue with start animations which
                                    // are randomly not played on fast devices in both Chrome and FF,
                                    // couldn't find a way to solve it yet. It seems that applying
                                    // the classes before appending to the DOM helps a little, but
                                    // it messes up some CSS transitions. The issue almost never
                                    // happens when delay[0]==0 though
                                    self._$tooltip
                                            .addClass('tooltipster-' + self.__options.animation)
                                            .addClass('tooltipster-initial')
                                            .css({
                                                '-moz-animation-duration': self.__options.animationDuration[0] + 'ms',
                                                '-ms-animation-duration': self.__options.animationDuration[0] + 'ms',
                                                '-o-animation-duration': self.__options.animationDuration[0] + 'ms',
                                                '-webkit-animation-duration': self.__options.animationDuration[0] + 'ms',
                                                'animation-duration': self.__options.animationDuration[0] + 'ms',
                                                'transition-duration': self.__options.animationDuration[0] + 'ms'
                                            });

                                    setTimeout(
                                            function () {

                                                // a quick hover may have already triggered a mouseleave
                                                if (self.__state != 'closed') {

                                                    self._$tooltip
                                                            .addClass('tooltipster-show')
                                                            .removeClass('tooltipster-initial');

                                                    if (self.__options.animationDuration[0] > 0) {
                                                        self._$tooltip.delay(self.__options.animationDuration[0]);
                                                    }

                                                    self._$tooltip.queue(finish);
                                                }
                                            },
                                            0
                                            );
                                } else {

                                    // old browsers will have to live with this
                                    self._$tooltip
                                            .css('display', 'none')
                                            .fadeIn(self.__options.animationDuration[0], finish);
                                }

                                // checks if the origin is removed while the tooltip is open
                                self.__trackerStart();

                                // NOTE: the listeners below have a '-triggerClose' namespace
                                // because we'll remove them when the tooltip closes (unlike
                                // the '-triggerOpen' listeners). So some of them are actually
                                // not about close triggers, rather about positioning.

                                $(env.window)
                                        // reposition on resize
                                        .on('resize.' + self.__namespace + '-triggerClose', function (e) {

                                            var $ae = $(document.activeElement);

                                            // reposition only if the resize event was not triggered upon the opening
                                            // of a virtual keyboard due to an input field being focused within the tooltip
                                            // (otherwise the repositioning would lose the focus)
                                            if ((!$ae.is('input') && !$ae.is('textarea'))
                                                    || !$.contains(self._$tooltip[0], $ae[0])
                                                    ) {
                                                self.reposition(e);
                                            }
                                        })
                                        // same as below for parents
                                        .on('scroll.' + self.__namespace + '-triggerClose', function (e) {
                                            self.__scrollHandler(e);
                                        });

                                self.__$originParents = self._$origin.parents();

                                // scrolling may require the tooltip to be moved or even
                                // repositioned in some cases
                                self.__$originParents.each(function (i, parent) {

                                    $(parent).on('scroll.' + self.__namespace + '-triggerClose', function (e) {
                                        self.__scrollHandler(e);
                                    });
                                });

                                if (self.__options.triggerClose.mouseleave
                                        || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
                                        ) {

                                    // we use an event to allow users/plugins to control when the mouseleave/touchleave
                                    // close triggers will come to action. It allows to have more triggering elements
                                    // than just the origin and the tooltip for example, or to cancel/delay the closing,
                                    // or to make the tooltip interactive even if it wasn't when it was open, etc.
                                    self._on('dismissable', function (event) {

                                        if (event.dismissable) {

                                            if (event.delay) {

                                                timeout = setTimeout(function () {
                                                    // event.event may be undefined
                                                    self._close(event.event);
                                                }, event.delay);

                                                self.__timeouts.close.push(timeout);
                                            } else {
                                                self._close(event);
                                            }
                                        } else {
                                            clearTimeout(timeout);
                                        }
                                    });

                                    // now set the listeners that will trigger 'dismissable' events
                                    var $elements = self._$origin,
                                            eventNamesIn = '',
                                            eventNamesOut = '',
                                            timeout = null;

                                    // if we have to allow interaction, bind on the tooltip too
                                    if (self.__options.interactive) {
                                        $elements = $elements.add(self._$tooltip);
                                    }

                                    if (self.__options.triggerClose.mouseleave) {
                                        eventNamesIn += 'mouseenter.' + self.__namespace + '-triggerClose ';
                                        eventNamesOut += 'mouseleave.' + self.__namespace + '-triggerClose ';
                                    }
                                    if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
                                        eventNamesIn += 'touchstart.' + self.__namespace + '-triggerClose';
                                        eventNamesOut += 'touchend.' + self.__namespace + '-triggerClose touchcancel.' + self.__namespace + '-triggerClose';
                                    }

                                    $elements
                                            // close after some time spent outside of the elements
                                            .on(eventNamesOut, function (event) {

                                                // it's ok if the touch gesture ended up to be a swipe,
                                                // it's still a "touch leave" situation
                                                if (self._touchIsTouchEvent(event)
                                                        || !self._touchIsEmulatedEvent(event)
                                                        ) {

                                                    var delay = (event.type == 'mouseleave') ?
                                                            self.__options.delay :
                                                            self.__options.delayTouch;

                                                    self._trigger({
                                                        delay: delay[1],
                                                        dismissable: true,
                                                        event: event,
                                                        type: 'dismissable'
                                                    });
                                                }
                                            })
                                            // suspend the mouseleave timeout when the pointer comes back
                                            // over the elements
                                            .on(eventNamesIn, function (event) {

                                                // it's also ok if the touch event is a swipe gesture
                                                if (self._touchIsTouchEvent(event)
                                                        || !self._touchIsEmulatedEvent(event)
                                                        ) {
                                                    self._trigger({
                                                        dismissable: false,
                                                        event: event,
                                                        type: 'dismissable'
                                                    });
                                                }
                                            });
                                }

                                // close the tooltip when the origin gets a mouse click (common behavior of
                                // native tooltips)
                                if (self.__options.triggerClose.originClick) {

                                    self._$origin.on('click.' + self.__namespace + '-triggerClose', function (event) {

                                        // we could actually let a tap trigger this but this feature just
                                        // does not make sense on touch devices
                                        if (!self._touchIsTouchEvent(event)
                                                && !self._touchIsEmulatedEvent(event)
                                                ) {
                                            self._close(event);
                                        }
                                    });
                                }

                                // set the same bindings for click and touch on the body to close the tooltip
                                if (self.__options.triggerClose.click
                                        || (self.__options.triggerClose.tap && env.hasTouchCapability)
                                        ) {

                                    // don't set right away since the click/tap event which triggered this method
                                    // (if it was a click/tap) is going to bubble up to the body, we don't want it
                                    // to close the tooltip immediately after it opened
                                    setTimeout(function () {

                                        if (self.__state != 'closed') {

                                            var eventNames = '',
                                                    $body = $(env.window.document.body);

                                            if (self.__options.triggerClose.click) {
                                                eventNames += 'click.' + self.__namespace + '-triggerClose ';
                                            }
                                            if (self.__options.triggerClose.tap && env.hasTouchCapability) {
                                                eventNames += 'touchend.' + self.__namespace + '-triggerClose';
                                            }

                                            $body.on(eventNames, function (event) {

                                                if (self._touchIsMeaningfulEvent(event)) {

                                                    self._touchRecordEvent(event);

                                                    if (!self.__options.interactive || !$.contains(self._$tooltip[0], event.target)) {
                                                        self._close(event);
                                                    }
                                                }
                                            });

                                            // needed to detect and ignore swiping
                                            if (self.__options.triggerClose.tap && env.hasTouchCapability) {

                                                $body.on('touchstart.' + self.__namespace + '-triggerClose', function (event) {
                                                    self._touchRecordEvent(event);
                                                });
                                            }
                                        }
                                    }, 0);
                                }

                                self._trigger('ready');

                                // call our custom callback
                                if (self.__options.functionReady) {
                                    self.__options.functionReady.call(self, self, {
                                        origin: self._$origin[0],
                                        tooltip: self._$tooltip[0]
                                    });
                                }
                            }

                            // if we have a timer set, let the countdown begin
                            if (self.__options.timer > 0) {

                                var timeout = setTimeout(function () {
                                    self._close();
                                }, self.__options.timer + extraTime);

                                self.__timeouts.close.push(timeout);
                            }
                        }
                    }
                }
            }

            return self;
        },

        /**
         * When using the mouseenter/touchstart open triggers, this function will
         * schedule the opening of the tooltip after the delay, if there is one
         *
         * @param event
         * @returns {self}
         * @protected
         */
        _openShortly: function (event) {

            var self = this,
                    ok = true;

            if (self.__state != 'stable' && self.__state != 'appearing') {

                // if a timeout is not already running
                if (!self.__timeouts.open) {

                    self._trigger({
                        type: 'start',
                        event: event,
                        stop: function () {
                            ok = false;
                        }
                    });

                    if (ok) {

                        var delay = (event.type.indexOf('touch') == 0) ?
                                self.__options.delayTouch :
                                self.__options.delay;

                        if (delay[0]) {

                            self.__timeouts.open = setTimeout(function () {

                                self.__timeouts.open = null;

                                // open only if the pointer (mouse or touch) is still over the origin.
                                // The check on the "meaningful event" can only be made here, after some
                                // time has passed (to know if the touch was a swipe or not)
                                if (self.__pointerIsOverOrigin && self._touchIsMeaningfulEvent(event)) {

                                    // signal that we go on
                                    self._trigger('startend');

                                    self._open(event);
                                } else {
                                    // signal that we cancel
                                    self._trigger('startcancel');
                                }
                            }, delay[0]);
                        } else {
                            // signal that we go on
                            self._trigger('startend');

                            self._open(event);
                        }
                    }
                }
            }

            return self;
        },

        /**
         * Meant for plugins to get their options
         * 
         * @param {string} pluginName The name of the plugin that asks for its options
         * @param {object} defaultOptions The default options of the plugin
         * @returns {object} The options
         * @protected
         */
        _optionsExtract: function (pluginName, defaultOptions) {

            var self = this,
                    options = $.extend(true, {}, defaultOptions);

            // if the plugin options were isolated in a property named after the
            // plugin, use them (prevents conflicts with other plugins)
            var pluginOptions = self.__options[pluginName];

            // if not, try to get them as regular options
            if (!pluginOptions) {

                pluginOptions = {};

                $.each(defaultOptions, function (optionName, value) {

                    var o = self.__options[optionName];

                    if (o !== undefined) {
                        pluginOptions[optionName] = o;
                    }
                });
            }

            // let's merge the default options and the ones that were provided. We'd want
            // to do a deep copy but not let jQuery merge arrays, so we'll do a shallow
            // extend on two levels, that will be enough if options are not more than 1
            // level deep
            $.each(options, function (optionName, value) {

                if (pluginOptions[optionName] !== undefined) {

                    if ((typeof value == 'object'
                            && !(value instanceof Array)
                            && value != null
                            )
                            &&
                            (typeof pluginOptions[optionName] == 'object'
                                    && !(pluginOptions[optionName] instanceof Array)
                                    && pluginOptions[optionName] != null
                                    )
                            ) {
                        $.extend(options[optionName], pluginOptions[optionName]);
                    } else {
                        options[optionName] = pluginOptions[optionName];
                    }
                }
            });

            return options;
        },

        /**
         * Used at instantiation of the plugin, or afterwards by plugins that activate themselves
         * on existing instances
         * 
         * @param {object} pluginName
         * @returns {self}
         * @protected
         */
        _plug: function (pluginName) {

            var plugin = $.tooltipster._plugin(pluginName);

            if (plugin) {

                // if there is a constructor for instances
                if (plugin.instance) {

                    // proxy non-private methods on the instance to allow new instance methods
                    $.tooltipster.__bridge(plugin.instance, this, plugin.name);
                }
            } else {
                throw new Error('The "' + pluginName + '" plugin is not defined');
            }

            return this;
        },

        /**
         * This will return true if the event is a mouse event which was
         * emulated by the browser after a touch event. This allows us to
         * really dissociate mouse and touch triggers.
         * 
         * There is a margin of error if a real mouse event is fired right
         * after (within the delay shown below) a touch event on the same
         * element, but hopefully it should not happen often.
         * 
         * @returns {boolean}
         * @protected
         */
        _touchIsEmulatedEvent: function (event) {

            var isEmulated = false,
                    now = new Date().getTime();

            for (var i = this.__touchEvents.length - 1; i >= 0; i--) {

                var e = this.__touchEvents[i];

                // delay, in milliseconds. It's supposed to be 300ms in
                // most browsers (350ms on iOS) to allow a double tap but
                // can be less (check out FastClick for more info)
                if (now - e.time < 500) {

                    if (e.target === event.target) {
                        isEmulated = true;
                    }
                } else {
                    break;
                }
            }

            return isEmulated;
        },

        /**
         * Returns false if the event was an emulated mouse event or
         * a touch event involved in a swipe gesture.
         * 
         * @param {object} event
         * @returns {boolean}
         * @protected
         */
        _touchIsMeaningfulEvent: function (event) {
            return (
                    (this._touchIsTouchEvent(event) && !this._touchSwiped(event.target))
                    || (!this._touchIsTouchEvent(event) && !this._touchIsEmulatedEvent(event))
                    );
        },

        /**
         * Checks if an event is a touch event
         * 
         * @param {object} event
         * @returns {boolean}
         * @protected
         */
        _touchIsTouchEvent: function (event) {
            return event.type.indexOf('touch') == 0;
        },

        /**
         * Store touch events for a while to detect swiping and emulated mouse events
         * 
         * @param {object} event
         * @returns {self}
         * @protected
         */
        _touchRecordEvent: function (event) {

            if (this._touchIsTouchEvent(event)) {
                event.time = new Date().getTime();
                this.__touchEvents.push(event);
            }

            return this;
        },

        /**
         * Returns true if a swipe happened after the last touchstart event fired on
         * event.target.
         * 
         * We need to differentiate a swipe from a tap before we let the event open
         * or close the tooltip. A swipe is when a touchmove (scroll) event happens
         * on the body between the touchstart and the touchend events of an element.
         * 
         * @param {object} target The HTML element that may have triggered the swipe
         * @returns {boolean}
         * @protected
         */
        _touchSwiped: function (target) {

            var swiped = false;

            for (var i = this.__touchEvents.length - 1; i >= 0; i--) {

                var e = this.__touchEvents[i];

                if (e.type == 'touchmove') {
                    swiped = true;
                    break;
                } else if (
                        e.type == 'touchstart'
                        && target === e.target
                        ) {
                    break;
                }
            }

            return swiped;
        },

        /**
         * Triggers an event on the instance emitters
         * 
         * @returns {self}
         * @protected
         */
        _trigger: function () {

            var args = Array.prototype.slice.apply(arguments);

            if (typeof args[0] == 'string') {
                args[0] = {type: args[0]};
            }

            // add properties to the event
            args[0].instance = this;
            args[0].origin = this._$origin ? this._$origin[0] : null;
            args[0].tooltip = this._$tooltip ? this._$tooltip[0] : null;

            // note: the order of emitters matters
            this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
            $.tooltipster._trigger.apply($.tooltipster, args);
            this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);

            return this;
        },

        /**
         * Deactivate a plugin on this instance
         * 
         * @returns {self}
         * @protected
         */
        _unplug: function (pluginName) {

            var self = this;

            // if the plugin has been activated on this instance
            if (self[pluginName]) {

                var plugin = $.tooltipster._plugin(pluginName);

                // if there is a constructor for instances
                if (plugin.instance) {

                    // unbridge
                    $.each(plugin.instance, function (methodName, fn) {

                        // if the method exists (privates methods do not) and comes indeed from
                        // this plugin (may be missing or come from a conflicting plugin).
                        if (self[methodName]
                                && self[methodName].bridged === self[pluginName]
                                ) {
                            delete self[methodName];
                        }
                    });
                }

                // destroy the plugin
                if (self[pluginName].__destroy) {
                    self[pluginName].__destroy();
                }

                // remove the reference to the plugin instance
                delete self[pluginName];
            }

            return self;
        },

        /**
         * @see self::_close
         * @returns {self}
         * @public
         */
        close: function (callback) {

            if (!this.__destroyed) {
                this._close(null, callback);
            } else {
                this.__destroyError();
            }

            return this;
        },

        /**
         * Sets or gets the content of the tooltip
         * 
         * @returns {mixed|self}
         * @public
         */
        content: function (content) {

            var self = this;

            // getter method
            if (content === undefined) {
                return self.__Content;
            }
            // setter method
            else {

                if (!self.__destroyed) {

                    // change the content
                    self.__contentSet(content);

                    if (self.__Content !== null) {

                        // update the tooltip if it is open
                        if (self.__state !== 'closed') {

                            // reset the content in the tooltip
                            self.__contentInsert();

                            // reposition and resize the tooltip
                            self.reposition();

                            // if we want to play a little animation showing the content changed
                            if (self.__options.updateAnimation) {

                                if (env.hasTransitions) {

                                    // keep the reference in the local scope
                                    var animation = self.__options.updateAnimation;

                                    self._$tooltip.addClass('tooltipster-update-' + animation);

                                    // remove the class after a while. The actual duration of the
                                    // update animation may be shorter, it's set in the CSS rules
                                    setTimeout(function () {

                                        if (self.__state != 'closed') {

                                            self._$tooltip.removeClass('tooltipster-update-' + animation);
                                        }
                                    }, 1000);
                                } else {
                                    self._$tooltip.fadeTo(200, 0.5, function () {
                                        if (self.__state != 'closed') {
                                            self._$tooltip.fadeTo(200, 1);
                                        }
                                    });
                                }
                            }
                        }
                    } else {
                        self._close();
                    }
                } else {
                    self.__destroyError();
                }

                return self;
            }
        },

        /**
         * Destroys the tooltip
         * 
         * @returns {self}
         * @public
         */
        destroy: function () {

            var self = this;

            if (!self.__destroyed) {

                if (self.__state != 'closed') {

                    // no closing delay
                    self.option('animationDuration', 0)
                            // force closing
                            ._close(null, null, true);
                } else {
                    // there might be an open timeout still running
                    self.__timeoutsClear();
                }

                // send event
                self._trigger('destroy');

                self.__destroyed = true;

                self._$origin
                        .removeData(self.__namespace)
                        // remove the open trigger listeners
                        .off('.' + self.__namespace + '-triggerOpen');

                // remove the touch listener
                $(env.window.document.body).off('.' + self.__namespace + '-triggerOpen');

                var ns = self._$origin.data('tooltipster-ns');

                // if the origin has been removed from DOM, its data may
                // well have been destroyed in the process and there would
                // be nothing to clean up or restore
                if (ns) {

                    // if there are no more tooltips on this element
                    if (ns.length === 1) {

                        // optional restoration of a title attribute
                        var title = null;
                        if (self.__options.restoration == 'previous') {
                            title = self._$origin.data('tooltipster-initialTitle');
                        } else if (self.__options.restoration == 'current') {

                            // old school technique to stringify when outerHTML is not supported
                            title = (typeof self.__Content == 'string') ?
                                    self.__Content :
                                    $('<div></div>').append(self.__Content).html();
                        }

                        if (title) {
                            self._$origin.attr('title', title);
                        }

                        // final cleaning

                        self._$origin.removeClass('tooltipstered');

                        self._$origin
                                .removeData('tooltipster-ns')
                                .removeData('tooltipster-initialTitle');
                    } else {
                        // remove the instance namespace from the list of namespaces of
                        // tooltips present on the element
                        ns = $.grep(ns, function (el, i) {
                            return el !== self.__namespace;
                        });
                        self._$origin.data('tooltipster-ns', ns);
                    }
                }

                // last event
                self._trigger('destroyed');

                // unbind private and public event listeners
                self._off();
                self.off();

                // remove external references, just in case
                self.__Content = null;
                self.__$emitterPrivate = null;
                self.__$emitterPublic = null;
                self.__options.parent = null;
                self._$origin = null;
                self._$tooltip = null;

                // make sure the object is no longer referenced in there to prevent
                // memory leaks
                $.tooltipster.__instancesLatestArr = $.grep($.tooltipster.__instancesLatestArr, function (el, i) {
                    return self !== el;
                });

                clearInterval(self.__garbageCollector);
            } else {
                self.__destroyError();
            }

            // we return the scope rather than true so that the call to
            // .tooltipster('destroy') actually returns the matched elements
            // and applies to all of them
            return self;
        },

        /**
         * Disables the tooltip
         * 
         * @returns {self}
         * @public
         */
        disable: function () {

            if (!this.__destroyed) {

                // close first, in case the tooltip would not disappear on
                // its own (no close trigger)
                this._close();
                this.__enabled = false;

                return this;
            } else {
                this.__destroyError();
            }

            return this;
        },

        /**
         * Returns the HTML element of the origin
         *
         * @returns {self}
         * @public
         */
        elementOrigin: function () {

            if (!this.__destroyed) {
                return this._$origin[0];
            } else {
                this.__destroyError();
            }
        },

        /**
         * Returns the HTML element of the tooltip
         *
         * @returns {self}
         * @public
         */
        elementTooltip: function () {
            return this._$tooltip ? this._$tooltip[0] : null;
        },

        /**
         * Enables the tooltip
         * 
         * @returns {self}
         * @public
         */
        enable: function () {
            this.__enabled = true;
            return this;
        },

        /**
         * Alias, deprecated in 4.0.0
         * 
         * @param {function} callback
         * @returns {self}
         * @public
         */
        hide: function (callback) {
            return this.close(callback);
        },

        /**
         * Returns the instance
         * 
         * @returns {self}
         * @public
         */
        instance: function () {
            return this;
        },

        /**
         * For public use only, not to be used by plugins (use ::_off() instead)
         * 
         * @returns {self}
         * @public
         */
        off: function () {

            if (!this.__destroyed) {
                this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
            }

            return this;
        },

        /**
         * For public use only, not to be used by plugins (use ::_on() instead)
         *
         * @returns {self}
         * @public
         */
        on: function () {

            if (!this.__destroyed) {
                this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
            } else {
                this.__destroyError();
            }

            return this;
        },

        /**
         * For public use only, not to be used by plugins
         *
         * @returns {self}
         * @public
         */
        one: function () {

            if (!this.__destroyed) {
                this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
            } else {
                this.__destroyError();
            }

            return this;
        },

        /**
         * @see self::_open
         * @returns {self}
         * @public
         */
        open: function (callback) {

            if (!this.__destroyed) {
                this._open(null, callback);
            } else {
                this.__destroyError();
            }

            return this;
        },

        /**
         * Get or set options. For internal use and advanced users only.
         * 
         * @param {string} o Option name
         * @param {mixed} val optional A new value for the option
         * @return {mixed|self} If val is omitted, the value of the option
         * is returned, otherwise the instance itself is returned
         * @public
         */
        option: function (o, val) {

            // getter
            if (val === undefined) {
                return this.__options[o];
            }
            // setter
            else {

                if (!this.__destroyed) {

                    // change value
                    this.__options[o] = val;

                    // format
                    this.__optionsFormat();

                    // re-prepare the triggers if needed
                    if ($.inArray(o, ['trigger', 'triggerClose', 'triggerOpen']) >= 0) {
                        this.__prepareOrigin();
                    }

                    if (o === 'selfDestruction') {
                        this.__prepareGC();
                    }
                } else {
                    this.__destroyError();
                }

                return this;
            }
        },

        /**
         * This method is in charge of setting the position and size properties of the tooltip.
         * All the hard work is delegated to the display plugin.
         * Note: The tooltip may be detached from the DOM at the moment the method is called 
         * but must be attached by the end of the method call.
         * 
         * @param {object} event For internal use only. Defined if an event such as
         * window resizing triggered the repositioning
         * @param {boolean} tooltipIsDetached For internal use only. Set this to true if you
         * know that the tooltip not being in the DOM is not an issue (typically when the
         * tooltip element has just been created but has not been added to the DOM yet).
         * @returns {self}
         * @public
         */
        reposition: function (event, tooltipIsDetached) {

            var self = this;

            if (!self.__destroyed) {

                // if the tooltip is still open and the origin is still in the DOM
                if (self.__state != 'closed' && bodyContains(self._$origin)) {

                    // if the tooltip has not been removed from DOM manually (or if it
                    // has been detached on purpose)
                    if (tooltipIsDetached || bodyContains(self._$tooltip)) {

                        if (!tooltipIsDetached) {
                            // detach in case the tooltip overflows the window and adds
                            // scrollbars to it, so __geometry can be accurate
                            self._$tooltip.detach();
                        }

                        // refresh the geometry object before passing it as a helper
                        self.__Geometry = self.__geometry();

                        // let a plugin fo the rest
                        self._trigger({
                            type: 'reposition',
                            event: event,
                            helper: {
                                geo: self.__Geometry
                            }
                        });
                    }
                }
            } else {
                self.__destroyError();
            }

            return self;
        },

        /**
         * Alias, deprecated in 4.0.0
         *
         * @param callback
         * @returns {self}
         * @public
         */
        show: function (callback) {
            return this.open(callback);
        },

        /**
         * Returns some properties about the instance
         * 
         * @returns {object}
         * @public
         */
        status: function () {

            return {
                destroyed: this.__destroyed,
                enabled: this.__enabled,
                open: this.__state !== 'closed',
                state: this.__state
            };
        },

        /**
         * For public use only, not to be used by plugins
         *
         * @returns {self}
         * @public
         */
        triggerHandler: function () {

            if (!this.__destroyed) {
                this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
            } else {
                this.__destroyError();
            }

            return this;
        }
    };

    $.fn.tooltipster = function () {

        // for using in closures
        var args = Array.prototype.slice.apply(arguments),
                // common mistake: an HTML element can't be in several tooltips at the same time
                contentCloningWarning = 'You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.';

        // this happens with $(sel).tooltipster(...) when $(sel) does not match anything
        if (this.length === 0) {

            // still chainable
            return this;
        }
        // this happens when calling $(sel).tooltipster('methodName or options')
        // where $(sel) matches one or more elements
        else {

            // method calls
            if (typeof args[0] === 'string') {

                var v = '#*$~&';

                this.each(function () {

                    // retrieve the namepaces of the tooltip(s) that exist on that element.
                    // We will interact with the first tooltip only.
                    var ns = $(this).data('tooltipster-ns'),
                            // self represents the instance of the first tooltipster plugin
                            // associated to the current HTML object of the loop
                            self = ns ? $(this).data(ns[0]) : null;

                    // if the current element holds a tooltipster instance
                    if (self) {

                        if (typeof self[args[0]] === 'function') {

                            if (this.length > 1
                                    && args[0] == 'content'
                                    && (args[1] instanceof $
                                            || (typeof args[1] == 'object' && args[1] != null && args[1].tagName)
                                            )
                                    && !self.__options.contentCloning
                                    && self.__options.debug
                                    ) {
                                console.log(contentCloningWarning);
                            }

                            // note : args[1] and args[2] may not be defined
                            var resp = self[args[0]](args[1], args[2]);
                        } else {
                            throw new Error('Unknown method "' + args[0] + '"');
                        }

                        // if the function returned anything other than the instance
                        // itself (which implies chaining, except for the `instance` method)
                        if (resp !== self || args[0] === 'instance') {

                            v = resp;

                            // return false to stop .each iteration on the first element
                            // matched by the selector
                            return false;
                        }
                    } else {
                        throw new Error('You called Tooltipster\'s "' + args[0] + '" method on an uninitialized element');
                    }
                });

                return (v !== '#*$~&') ? v : this;
            }
            // first argument is undefined or an object: the tooltip is initializing
            else {

                // reset the array of last initialized objects
                $.tooltipster.__instancesLatestArr = [];

                // is there a defined value for the multiple option in the options object ?
                var multipleIsSet = args[0] && args[0].multiple !== undefined,
                        // if the multiple option is set to true, or if it's not defined but
                        // set to true in the defaults
                        multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple),
                        // same for content
                        contentIsSet = args[0] && args[0].content !== undefined,
                        content = (contentIsSet && args[0].content) || (!contentIsSet && defaults.content),
                        // same for contentCloning
                        contentCloningIsSet = args[0] && args[0].contentCloning !== undefined,
                        contentCloning =
                        (contentCloningIsSet && args[0].contentCloning)
                        || (!contentCloningIsSet && defaults.contentCloning),
                        // same for debug
                        debugIsSet = args[0] && args[0].debug !== undefined,
                        debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug);

                if (this.length > 1
                        && (content instanceof $
                                || (typeof content == 'object' && content != null && content.tagName)
                                )
                        && !contentCloning
                        && debug
                        ) {
                    console.log(contentCloningWarning);
                }

                // create a tooltipster instance for each element if it doesn't
                // already have one or if the multiple option is set, and attach the
                // object to it
                this.each(function () {

                    var go = false,
                            $this = $(this),
                            ns = $this.data('tooltipster-ns'),
                            obj = null;

                    if (!ns) {
                        go = true;
                    } else if (multiple) {
                        go = true;
                    } else if (debug) {
                        console.log('Tooltipster: one or more tooltips are already attached to the element below. Ignoring.');
                        //console.log(this);
                    }

                    if (go) {
                        obj = new $.Tooltipster(this, args[0]);

                        // save the reference of the new instance
                        if (!ns)
                            ns = [];
                        ns.push(obj.__namespace);
                        $this.data('tooltipster-ns', ns);

                        // save the instance itself
                        $this.data(obj.__namespace, obj);

                        // call our constructor custom function.
                        // we do this here and not in ::init() because we wanted
                        // the object to be saved in $this.data before triggering
                        // it
                        if (obj.__options.functionInit) {
                            obj.__options.functionInit.call(obj, obj, {
                                origin: this
                            });
                        }

                        // and now the event, for the plugins and core emitter
                        obj._trigger('init');
                    }

                    $.tooltipster.__instancesLatestArr.push(obj);
                });

                return this;
            }
        }
    };

// Utilities

    /**
     * A class to check if a tooltip can fit in given dimensions
     * 
     * @param {object} $tooltip The jQuery wrapped tooltip element, or a clone of it
     */
    function Ruler($tooltip) {

        // list of instance variables

        this.$container;
        this.constraints = null;
        this.__$tooltip;

        this.__init($tooltip);
    }

    Ruler.prototype = {

        /**
         * Move the tooltip into an invisible div that does not allow overflow to make
         * size tests. Note: the tooltip may or may not be attached to the DOM at the
         * moment this method is called, it does not matter.
         * 
         * @param {object} $tooltip The object to test. May be just a clone of the
         * actual tooltip.
         * @private
         */
        __init: function ($tooltip) {

            this.__$tooltip = $tooltip;

            this.__$tooltip
                    .css({
                        // for some reason we have to specify top and left 0
                        left: 0,
                        // any overflow will be ignored while measuring
                        overflow: 'hidden',
                        // positions at (0,0) without the div using 100% of the available width
                        position: 'absolute',
                        top: 0
                    })
                    // overflow must be auto during the test. We re-set this in case
                    // it were modified by the user
                    .find('.tooltipster-content')
                    .css('overflow', 'auto');

            this.$container = $('<div class="tooltipster-ruler"></div>')
                    .append(this.__$tooltip)
                    .appendTo(env.window.document.body);
        },

        /**
         * Force the browser to redraw (re-render) the tooltip immediately. This is required
         * when you changed some CSS properties and need to make something with it
         * immediately, without waiting for the browser to redraw at the end of instructions.
         *
         * @see http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes
         * @private
         */
        __forceRedraw: function () {

            // note: this would work but for Webkit only
            //this.__$tooltip.close();
            //this.__$tooltip[0].offsetHeight;
            //this.__$tooltip.open();

            // works in FF too
            var $p = this.__$tooltip.parent();
            this.__$tooltip.detach();
            this.__$tooltip.appendTo($p);
        },

        /**
         * Set maximum dimensions for the tooltip. A call to ::measure afterwards
         * will tell us if the content overflows or if it's ok
         *
         * @param {int} width
         * @param {int} height
         * @return {Ruler}
         * @public
         */
        constrain: function (width, height) {

            this.constraints = {
                width: width,
                height: height
            };

            this.__$tooltip.css({
                // we disable display:flex, otherwise the content would overflow without
                // creating horizontal scrolling (which we need to detect).
                display: 'block',
                // reset any previous height
                height: '',
                // we'll check if horizontal scrolling occurs
                overflow: 'auto',
                // we'll set the width and see what height is generated and if there
                // is horizontal overflow
                width: width
            });

            return this;
        },

        /**
         * Reset the tooltip content overflow and remove the test container
         * 
         * @returns {Ruler}
         * @public
         */
        destroy: function () {

            // in case the element was not a clone
            this.__$tooltip
                    .detach()
                    .find('.tooltipster-content')
                    .css({
                        // reset to CSS value
                        display: '',
                        overflow: ''
                    });

            this.$container.remove();
        },

        /**
         * Removes any constraints
         * 
         * @returns {Ruler}
         * @public
         */
        free: function () {

            this.constraints = null;

            // reset to natural size
            this.__$tooltip.css({
                display: '',
                height: '',
                overflow: 'visible',
                width: ''
            });

            return this;
        },

        /**
         * Returns the size of the tooltip. When constraints are applied, also returns
         * whether the tooltip fits in the provided dimensions.
         * The idea is to see if the new height is small enough and if the content does
         * not overflow horizontally.
         *
         * @param {int} width
         * @param {int} height
         * @returns {object} An object with a bool `fits` property and a `size` property
         * @public
         */
        measure: function () {

            this.__forceRedraw();

            var tooltipBcr = this.__$tooltip[0].getBoundingClientRect(),
                    result = {size: {
                            // bcr.width/height are not defined in IE8- but in this
                            // case, bcr.right/bottom will have the same value
                            // except in iOS 8+ where tooltipBcr.bottom/right are wrong
                            // after scrolling for reasons yet to be determined.
                            // tooltipBcr.top/left might not be 0, see issue #514
                            height: tooltipBcr.height || (tooltipBcr.bottom - tooltipBcr.top),
                            width: tooltipBcr.width || (tooltipBcr.right - tooltipBcr.left)
                        }};

            if (this.constraints) {

                // note: we used to use offsetWidth instead of boundingRectClient but
                // it returned rounded values, causing issues with sub-pixel layouts.

                // note2: noticed that the bcrWidth of text content of a div was once
                // greater than the bcrWidth of its container by 1px, causing the final
                // tooltip box to be too small for its content. However, evaluating
                // their widths one against the other (below) surprisingly returned
                // equality. Happened only once in Chrome 48, was not able to reproduce
                // => just having fun with float position values...

                var $content = this.__$tooltip.find('.tooltipster-content'),
                        height = this.__$tooltip.outerHeight(),
                        contentBcr = $content[0].getBoundingClientRect(),
                        fits = {
                            height: height <= this.constraints.height,
                            width: (
                                    // this condition accounts for min-width property that
                                    // may apply
                                    tooltipBcr.width <= this.constraints.width
                                    // the -1 is here because scrollWidth actually returns
                                    // a rounded value, and may be greater than bcr.width if
                                    // it was rounded up. This may cause an issue for contents
                                    // which actually really overflow  by 1px or so, but that
                                    // should be rare. Not sure how to solve this efficiently.
                                    // See http://blogs.msdn.com/b/ie/archive/2012/02/17/sub-pixel-rendering-and-the-css-object-model.aspx
                                    && contentBcr.width >= $content[0].scrollWidth - 1
                                    )
                        };

                result.fits = fits.height && fits.width;
            }

            // old versions of IE get the width wrong for some reason and it causes
            // the text to be broken to a new line, so we round it up. If the width
            // is the width of the screen though, we can assume it is accurate.
            if (env.IE
                    && env.IE <= 11
                    && result.size.width !== env.window.document.documentElement.clientWidth
                    ) {
                result.size.width = Math.ceil(result.size.width) + 1;
            }

            return result;
        }
    };

// quick & dirty compare function, not bijective nor multidimensional
    function areEqual(a, b) {
        var same = true;
        $.each(a, function (i, _) {
            if (b[i] === undefined || a[i] !== b[i]) {
                same = false;
                return false;
            }
        });
        return same;
    }

    /**
     * A fast function to check if an element is still in the DOM. It
     * tries to use an id as ids are indexed by the browser, or falls
     * back to jQuery's `contains` method. May fail if two elements
     * have the same id, but so be it
     *
     * @param {object} $obj A jQuery-wrapped HTML element
     * @return {boolean}
     */
    function bodyContains($obj) {
        var id = $obj.attr('id'),
                el = id ? env.window.document.getElementById(id) : null;
        // must also check that the element with the id is the one we want
        return el ? el === $obj[0] : $.contains(env.window.document.body, $obj[0]);
    }

// detect IE versions for dirty fixes
    var uA = navigator.userAgent.toLowerCase();
    if (uA.indexOf('msie') != -1)
        env.IE = parseInt(uA.split('msie')[1]);
    else if (uA.toLowerCase().indexOf('trident') !== -1 && uA.indexOf(' rv:11') !== -1)
        env.IE = 11;
    else if (uA.toLowerCase().indexOf('edge/') != -1)
        env.IE = parseInt(uA.toLowerCase().split('edge/')[1]);

// detecting support for CSS transitions
    function transitionSupport() {

        // env.window is not defined yet when this is called
        if (!win)
            return false;

        var b = win.document.body || win.document.documentElement,
                s = b.style,
                p = 'transition',
                v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'];

        if (typeof s[p] == 'string') {
            return true;
        }

        p = p.charAt(0).toUpperCase() + p.substr(1);
        for (var i = 0; i < v.length; i++) {
            if (typeof s[v[i] + p] == 'string') {
                return true;
            }
        }
        return false;
    }

// we'll return jQuery for plugins not to have to declare it as a dependency,
// but it's done by a build task since it should be included only once at the
// end when we concatenate the main file with a plugin
// sideTip is Tooltipster's default plugin.
// This file will be UMDified by a build task.

    var pluginName = 'tooltipster.sideTip';

    $.tooltipster._plugin({
        name: pluginName,
        instance: {
            /**
             * Defaults are provided as a function for an easy override by inheritance
             *
             * @return {object} An object with the defaults options
             * @private
             */
            __defaults: function () {

                return {
                    // if the tooltip should display an arrow that points to the origin
                    arrow: true,
                    // the distance in pixels between the tooltip and the origin
                    distance: 6,
                    // allows to easily change the position of the tooltip
                    functionPosition: null,
                    maxWidth: null,
                    // used to accomodate the arrow of tooltip if there is one.
                    // First to make sure that the arrow target is not too close
                    // to the edge of the tooltip, so the arrow does not overflow
                    // the tooltip. Secondly when we reposition the tooltip to
                    // make sure that it's positioned in such a way that the arrow is
                    // still pointing at the target (and not a few pixels beyond it).
                    // It should be equal to or greater than half the width of
                    // the arrow (by width we mean the size of the side which touches
                    // the side of the tooltip).
                    minIntersection: 16,
                    minWidth: 0,
                    // deprecated in 4.0.0. Listed for _optionsExtract to pick it up
                    position: null,
                    side: 'top',
                    // set to false to position the tooltip relatively to the document rather
                    // than the window when we open it
                    viewportAware: true
                };
            },

            /**
             * Run once: at instantiation of the plugin
             *
             * @param {object} instance The tooltipster object that instantiated this plugin
             * @private
             */
            __init: function (instance) {

                var self = this;

                // list of instance variables

                self.__instance = instance;
                self.__namespace = 'tooltipster-sideTip-' + Math.round(Math.random() * 1000000);
                self.__previousState = 'closed';
                self.__options;

                // initial formatting
                self.__optionsFormat();

                self.__instance._on('state.' + self.__namespace, function (event) {

                    if (event.state == 'closed') {
                        self.__close();
                    } else if (event.state == 'appearing' && self.__previousState == 'closed') {
                        self.__create();
                    }

                    self.__previousState = event.state;
                });

                // reformat every time the options are changed
                self.__instance._on('options.' + self.__namespace, function () {
                    self.__optionsFormat();
                });

                self.__instance._on('reposition.' + self.__namespace, function (e) {
                    self.__reposition(e.event, e.helper);
                });
            },

            /**
             * Called when the tooltip has closed
             * 
             * @private
             */
            __close: function () {

                // detach our content object first, so the next jQuery's remove()
                // call does not unbind its event handlers
                if (this.__instance.content() instanceof $) {
                    this.__instance.content().detach();
                }

                // remove the tooltip from the DOM
                this.__instance._$tooltip.remove();
                this.__instance._$tooltip = null;
            },

            /**
             * Creates the HTML element of the tooltip.
             * 
             * @private
             */
            __create: function () {

                // note: we wrap with a .tooltipster-box div to be able to set a margin on it
                // (.tooltipster-base must not have one)
                var $html = $(
                        '<div class="tooltipster-base tooltipster-sidetip">' +
                        '<div class="tooltipster-box">' +
                        '<div class="tooltipster-content"></div>' +
                        '</div>' +
                        '<div class="tooltipster-arrow">' +
                        '<div class="tooltipster-arrow-uncropped">' +
                        '<div class="tooltipster-arrow-border"></div>' +
                        '<div class="tooltipster-arrow-background"></div>' +
                        '</div>' +
                        '</div>' +
                        '</div>'
                        );

                // hide arrow if asked
                if (!this.__options.arrow) {
                    $html
                            .find('.tooltipster-box')
                            .css('margin', 0)
                            .end()
                            .find('.tooltipster-arrow')
                            .hide();
                }

                // apply min/max width if asked
                if (this.__options.minWidth) {
                    $html.css('min-width', this.__options.minWidth + 'px');
                }
                if (this.__options.maxWidth) {
                    $html.css('max-width', this.__options.maxWidth + 'px');
                }

                this.__instance._$tooltip = $html;

                // tell the instance that the tooltip element has been created
                this.__instance._trigger('created');
            },

            /**
             * Used when the plugin is to be unplugged
             *
             * @private
             */
            __destroy: function () {
                this.__instance._off('.' + self.__namespace);
            },

            /**
             * (Re)compute this.__options from the options declared to the instance
             *
             * @private
             */
            __optionsFormat: function () {

                var self = this;

                // get the options
                self.__options = self.__instance._optionsExtract(pluginName, self.__defaults());

                // for backward compatibility, deprecated in v4.0.0
                if (self.__options.position) {
                    self.__options.side = self.__options.position;
                }

                // options formatting

                // format distance as a four-cell array if it ain't one yet and then make
                // it an object with top/bottom/left/right properties
                if (typeof self.__options.distance != 'object') {
                    self.__options.distance = [self.__options.distance];
                }
                if (self.__options.distance.length < 4) {

                    if (self.__options.distance[1] === undefined)
                        self.__options.distance[1] = self.__options.distance[0];
                    if (self.__options.distance[2] === undefined)
                        self.__options.distance[2] = self.__options.distance[0];
                    if (self.__options.distance[3] === undefined)
                        self.__options.distance[3] = self.__options.distance[1];

                    self.__options.distance = {
                        top: self.__options.distance[0],
                        right: self.__options.distance[1],
                        bottom: self.__options.distance[2],
                        left: self.__options.distance[3]
                    };
                }

                // let's transform:
                // 'top' into ['top', 'bottom', 'right', 'left']
                // 'right' into ['right', 'left', 'top', 'bottom']
                // 'bottom' into ['bottom', 'top', 'right', 'left']
                // 'left' into ['left', 'right', 'top', 'bottom']
                if (typeof self.__options.side == 'string') {

                    var opposites = {
                        'top': 'bottom',
                        'right': 'left',
                        'bottom': 'top',
                        'left': 'right'
                    };

                    self.__options.side = [self.__options.side, opposites[self.__options.side]];

                    if (self.__options.side[0] == 'left' || self.__options.side[0] == 'right') {
                        self.__options.side.push('top', 'bottom');
                    } else {
                        self.__options.side.push('right', 'left');
                    }
                }

                // misc
                // disable the arrow in IE6 unless the arrow option was explicitly set to true
                if ($.tooltipster._env.IE === 6
                        && self.__options.arrow !== true
                        ) {
                    self.__options.arrow = false;
                }
            },

            /**
             * This method must compute and set the positioning properties of the
             * tooltip (left, top, width, height, etc.). It must also make sure the
             * tooltip is eventually appended to its parent (since the element may be
             * detached from the DOM at the moment the method is called).
             *
             * We'll evaluate positioning scenarios to find which side can contain the
             * tooltip in the best way. We'll consider things relatively to the window
             * (unless the user asks not to), then to the document (if need be, or if the
             * user explicitly requires the tests to run on the document). For each
             * scenario, measures are taken, allowing us to know how well the tooltip
             * is going to fit. After that, a sorting function will let us know what
             * the best scenario is (we also allow the user to choose his favorite
             * scenario by using an event).
             * 
             * @param {object} helper An object that contains variables that plugin
             * creators may find useful (see below)
             * @param {object} helper.geo An object with many layout properties
             * about objects of interest (window, document, origin). This should help
             * plugin users compute the optimal position of the tooltip
             * @private
             */
            __reposition: function (event, helper) {

                var self = this,
                        finalResult,
                        // to know where to put the tooltip, we need to know on which point
                        // of the x or y axis we should center it. That coordinate is the target
                        targets = self.__targetFind(helper),
                        testResults = [];

                // make sure the tooltip is detached while we make tests on a clone
                self.__instance._$tooltip.detach();

                // we could actually provide the original element to the Ruler and
                // not a clone, but it just feels right to keep it out of the
                // machinery.
                var $clone = self.__instance._$tooltip.clone(),
                        // start position tests session
                        ruler = $.tooltipster._getRuler($clone),
                        satisfied = false,
                        animation = self.__instance.option('animation');

                // an animation class could contain properties that distort the size
                if (animation) {
                    $clone.removeClass('tooltipster-' + animation);
                }

                // start evaluating scenarios
                $.each(['window', 'document'], function (i, container) {

                    var takeTest = null;

                    // let the user decide to keep on testing or not
                    self.__instance._trigger({
                        container: container,
                        helper: helper,
                        satisfied: satisfied,
                        takeTest: function (bool) {
                            takeTest = bool;
                        },
                        results: testResults,
                        type: 'positionTest'
                    });

                    if (takeTest == true
                            || (takeTest != false
                                    && satisfied == false
                                    // skip the window scenarios if asked. If they are reintegrated by
                                    // the callback of the positionTest event, they will have to be
                                    // excluded using the callback of positionTested
                                    && (container != 'window' || self.__options.viewportAware)
                                    )
                            ) {

                        // for each allowed side
                        for (var i = 0; i < self.__options.side.length; i++) {

                            var distance = {
                                horizontal: 0,
                                vertical: 0
                            },
                                    side = self.__options.side[i];

                            if (side == 'top' || side == 'bottom') {
                                distance.vertical = self.__options.distance[side];
                            } else {
                                distance.horizontal = self.__options.distance[side];
                            }

                            // this may have an effect on the size of the tooltip if there are css
                            // rules for the arrow or something else
                            self.__sideChange($clone, side);

                            $.each(['natural', 'constrained'], function (i, mode) {

                                takeTest = null;

                                // emit an event on the instance
                                self.__instance._trigger({
                                    container: container,
                                    event: event,
                                    helper: helper,
                                    mode: mode,
                                    results: testResults,
                                    satisfied: satisfied,
                                    side: side,
                                    takeTest: function (bool) {
                                        takeTest = bool;
                                    },
                                    type: 'positionTest'
                                });

                                if (takeTest == true
                                        || (takeTest != false
                                                && satisfied == false
                                                )
                                        ) {

                                    var testResult = {
                                        container: container,
                                        // we let the distance as an object here, it can make things a little easier
                                        // during the user's calculations at positionTest/positionTested
                                        distance: distance,
                                        // whether the tooltip can fit in the size of the viewport (does not mean
                                        // that we'll be able to make it initially entirely visible, see 'whole')
                                        fits: null,
                                        mode: mode,
                                        outerSize: null,
                                        side: side,
                                        size: null,
                                        target: targets[side],
                                        // check if the origin has enough surface on screen for the tooltip to
                                        // aim at it without overflowing the viewport (this is due to the thickness
                                        // of the arrow represented by the minIntersection length).
                                        // If not, the tooltip will have to be partly or entirely off screen in
                                        // order to stay docked to the origin. This value will stay null when the
                                        // container is the document, as it is not relevant
                                        whole: null
                                    };

                                    // get the size of the tooltip with or without size constraints
                                    var rulerConfigured = (mode == 'natural') ?
                                            ruler.free() :
                                            ruler.constrain(
                                                    helper.geo.available[container][side].width - distance.horizontal,
                                                    helper.geo.available[container][side].height - distance.vertical
                                                    ),
                                            rulerResults = rulerConfigured.measure();

                                    testResult.size = rulerResults.size;
                                    testResult.outerSize = {
                                        height: rulerResults.size.height + distance.vertical,
                                        width: rulerResults.size.width + distance.horizontal
                                    };

                                    if (mode == 'natural') {

                                        if (helper.geo.available[container][side].width >= testResult.outerSize.width
                                                && helper.geo.available[container][side].height >= testResult.outerSize.height
                                                ) {
                                            testResult.fits = true;
                                        } else {
                                            testResult.fits = false;
                                        }
                                    } else {
                                        testResult.fits = rulerResults.fits;
                                    }

                                    if (container == 'window') {

                                        if (!testResult.fits) {
                                            testResult.whole = false;
                                        } else {
                                            if (side == 'top' || side == 'bottom') {

                                                testResult.whole = (
                                                        helper.geo.origin.windowOffset.right >= self.__options.minIntersection
                                                        && helper.geo.window.size.width - helper.geo.origin.windowOffset.left >= self.__options.minIntersection
                                                        );
                                            } else {
                                                testResult.whole = (
                                                        helper.geo.origin.windowOffset.bottom >= self.__options.minIntersection
                                                        && helper.geo.window.size.height - helper.geo.origin.windowOffset.top >= self.__options.minIntersection
                                                        );
                                            }
                                        }
                                    }

                                    testResults.push(testResult);

                                    // we don't need to compute more positions if we have one fully on screen
                                    if (testResult.whole) {
                                        satisfied = true;
                                    } else {
                                        // don't run the constrained test unless the natural width was greater
                                        // than the available width, otherwise it's pointless as we know it
                                        // wouldn't fit either
                                        if (testResult.mode == 'natural'
                                                && (testResult.fits
                                                        || testResult.size.width <= helper.geo.available[container][side].width
                                                        )
                                                ) {
                                            return false;
                                        }
                                    }
                                }
                            });
                        }
                    }
                });

                // the user may eliminate the unwanted scenarios from testResults, but he's
                // not supposed to alter them at this point. functionPosition and the
                // position event serve that purpose.
                self.__instance._trigger({
                    edit: function (r) {
                        testResults = r;
                    },
                    event: event,
                    helper: helper,
                    results: testResults,
                    type: 'positionTested'
                });

                /**
                 * Sort the scenarios to find the favorite one.
                 * 
                 * The favorite scenario is when we can fully display the tooltip on screen,
                 * even if it means that the middle of the tooltip is no longer centered on
                 * the middle of the origin (when the origin is near the edge of the screen
                 * or even partly off screen). We want the tooltip on the preferred side,
                 * even if it means that we have to use a constrained size rather than a
                 * natural one (as long as it fits). When the origin is off screen at the top
                 * the tooltip will be positioned at the bottom (if allowed), if the origin
                 * is off screen on the right, it will be positioned on the left, etc.
                 * If there are no scenarios where the tooltip can fit on screen, or if the
                 * user does not want the tooltip to fit on screen (viewportAware == false),
                 * we fall back to the scenarios relative to the document.
                 * 
                 * When the tooltip is bigger than the viewport in either dimension, we stop
                 * looking at the window scenarios and consider the document scenarios only,
                 * with the same logic to find on which side it would fit best.
                 * 
                 * If the tooltip cannot fit the document on any side, we force it at the
                 * bottom, so at least the user can scroll to see it.
                 */
                testResults.sort(function (a, b) {

                    // best if it's whole (the tooltip fits and adapts to the viewport)
                    if (a.whole && !b.whole) {
                        return -1;
                    } else if (!a.whole && b.whole) {
                        return 1;
                    } else if (a.whole && b.whole) {

                        var ai = self.__options.side.indexOf(a.side),
                                bi = self.__options.side.indexOf(b.side);

                        // use the user's sides fallback array
                        if (ai < bi) {
                            return -1;
                        } else if (ai > bi) {
                            return 1;
                        } else {
                            // will be used if the user forced the tests to continue
                            return a.mode == 'natural' ? -1 : 1;
                        }
                    } else {

                        // better if it fits
                        if (a.fits && !b.fits) {
                            return -1;
                        } else if (!a.fits && b.fits) {
                            return 1;
                        } else if (a.fits && b.fits) {

                            var ai = self.__options.side.indexOf(a.side),
                                    bi = self.__options.side.indexOf(b.side);

                            // use the user's sides fallback array
                            if (ai < bi) {
                                return -1;
                            } else if (ai > bi) {
                                return 1;
                            } else {
                                // will be used if the user forced the tests to continue
                                return a.mode == 'natural' ? -1 : 1;
                            }
                        } else {

                            // if everything failed, this will give a preference to the case where
                            // the tooltip overflows the document at the bottom
                            if (a.container == 'document'
                                    && a.side == 'bottom'
                                    && a.mode == 'natural'
                                    ) {
                                return -1;
                            } else {
                                return 1;
                            }
                        }
                    }
                });

                finalResult = testResults[0];


                // now let's find the coordinates of the tooltip relatively to the window
                finalResult.coord = {};

                switch (finalResult.side) {

                    case 'left':
                    case 'right':
                        finalResult.coord.top = Math.floor(finalResult.target - finalResult.size.height / 2);
                        break;

                    case 'bottom':
                    case 'top':
                        finalResult.coord.left = Math.floor(finalResult.target - finalResult.size.width / 2);
                        break;
                }

                switch (finalResult.side) {

                    case 'left':
                        finalResult.coord.left = helper.geo.origin.windowOffset.left - finalResult.outerSize.width;
                        break;

                    case 'right':
                        finalResult.coord.left = helper.geo.origin.windowOffset.right + finalResult.distance.horizontal;
                        break;

                    case 'top':
                        finalResult.coord.top = helper.geo.origin.windowOffset.top - finalResult.outerSize.height;
                        break;

                    case 'bottom':
                        finalResult.coord.top = helper.geo.origin.windowOffset.bottom + finalResult.distance.vertical;
                        break;
                }

                // if the tooltip can potentially be contained within the viewport dimensions
                // and that we are asked to make it fit on screen
                if (finalResult.container == 'window') {

                    // if the tooltip overflows the viewport, we'll move it accordingly (then it will
                    // not be centered on the middle of the origin anymore). We only move horizontally
                    // for top and bottom tooltips and vice versa.
                    if (finalResult.side == 'top' || finalResult.side == 'bottom') {

                        // if there is an overflow on the left
                        if (finalResult.coord.left < 0) {

                            // prevent the overflow unless the origin itself gets off screen (minus the
                            // margin needed to keep the arrow pointing at the target)
                            if (helper.geo.origin.windowOffset.right - this.__options.minIntersection >= 0) {
                                finalResult.coord.left = 0;
                            } else {
                                finalResult.coord.left = helper.geo.origin.windowOffset.right - this.__options.minIntersection - 1;
                            }
                        }
                        // or an overflow on the right
                        else if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) {

                            if (helper.geo.origin.windowOffset.left + this.__options.minIntersection <= helper.geo.window.size.width) {
                                finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width;
                            } else {
                                finalResult.coord.left = helper.geo.origin.windowOffset.left + this.__options.minIntersection + 1 - finalResult.size.width;
                            }
                        }
                    } else {

                        // overflow at the top
                        if (finalResult.coord.top < 0) {

                            if (helper.geo.origin.windowOffset.bottom - this.__options.minIntersection >= 0) {
                                finalResult.coord.top = 0;
                            } else {
                                finalResult.coord.top = helper.geo.origin.windowOffset.bottom - this.__options.minIntersection - 1;
                            }
                        }
                        // or at the bottom
                        else if (finalResult.coord.top > helper.geo.window.size.height - finalResult.size.height) {

                            if (helper.geo.origin.windowOffset.top + this.__options.minIntersection <= helper.geo.window.size.height) {
                                finalResult.coord.top = helper.geo.window.size.height - finalResult.size.height;
                            } else {
                                finalResult.coord.top = helper.geo.origin.windowOffset.top + this.__options.minIntersection + 1 - finalResult.size.height;
                            }
                        }
                    }
                } else {

                    // there might be overflow here too but it's easier to handle. If there has
                    // to be an overflow, we'll make sure it's on the right side of the screen
                    // (because the browser will extend the document size if there is an overflow
                    // on the right, but not on the left). The sort function above has already
                    // made sure that a bottom document overflow is preferred to a top overflow,
                    // so we don't have to care about it.

                    // if there is an overflow on the right
                    if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) {

                        // this may actually create on overflow on the left but we'll fix it in a sec
                        finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width;
                    }

                    // if there is an overflow on the left
                    if (finalResult.coord.left < 0) {

                        // don't care if it overflows the right after that, we made our best
                        finalResult.coord.left = 0;
                    }
                }


                // submit the positioning proposal to the user function which may choose to change
                // the side, size and/or the coordinates

                // first, set the rules that corresponds to the proposed side: it may change
                // the size of the tooltip, and the custom functionPosition may want to detect the
                // size of something before making a decision. So let's make things easier for the
                // implementor
                self.__sideChange($clone, finalResult.side);

                // add some variables to the helper
                helper.tooltipClone = $clone[0];
                helper.tooltipParent = self.__instance.option('parent').parent[0];
                // move informative values to the helper
                helper.mode = finalResult.mode;
                helper.whole = finalResult.whole;
                // add some variables to the helper for the functionPosition callback (these
                // will also be added to the event fired by self.__instance._trigger but that's
                // ok, we're just being consistent)
                helper.origin = self.__instance._$origin[0];
                helper.tooltip = self.__instance._$tooltip[0];

                // leave only the actionable values in there for functionPosition
                delete finalResult.container;
                delete finalResult.fits;
                delete finalResult.mode;
                delete finalResult.outerSize;
                delete finalResult.whole;

                // keep only the distance on the relevant side, for clarity
                finalResult.distance = finalResult.distance.horizontal || finalResult.distance.vertical;

                // beginners may not be comfortable with the concept of editing the object
                //  passed by reference, so we provide an edit function and pass a clone
                var finalResultClone = $.extend(true, {}, finalResult);

                // emit an event on the instance
                self.__instance._trigger({
                    edit: function (result) {
                        finalResult = result;
                    },
                    event: event,
                    helper: helper,
                    position: finalResultClone,
                    type: 'position'
                });

                if (self.__options.functionPosition) {

                    var result = self.__options.functionPosition.call(self, self.__instance, helper, finalResultClone);

                    if (result)
                        finalResult = result;
                }

                // end the positioning tests session (the user might have had a
                // use for it during the position event, now it's over)
                ruler.destroy();

                // compute the position of the target relatively to the tooltip root
                // element so we can place the arrow and make the needed adjustments
                var arrowCoord,
                        maxVal;

                if (finalResult.side == 'top' || finalResult.side == 'bottom') {

                    arrowCoord = {
                        prop: 'left',
                        val: finalResult.target - finalResult.coord.left
                    };
                    maxVal = finalResult.size.width - this.__options.minIntersection;
                } else {

                    arrowCoord = {
                        prop: 'top',
                        val: finalResult.target - finalResult.coord.top
                    };
                    maxVal = finalResult.size.height - this.__options.minIntersection;
                }

                // cannot lie beyond the boundaries of the tooltip, minus the
                // arrow margin
                if (arrowCoord.val < this.__options.minIntersection) {
                    arrowCoord.val = this.__options.minIntersection;
                } else if (arrowCoord.val > maxVal) {
                    arrowCoord.val = maxVal;
                }

                var originParentOffset;

                // let's convert the window-relative coordinates into coordinates relative to the
                // future positioned parent that the tooltip will be appended to
                if (helper.geo.origin.fixedLineage) {

                    // same as windowOffset when the position is fixed
                    originParentOffset = helper.geo.origin.windowOffset;
                } else {

                    // this assumes that the parent of the tooltip is located at
                    // (0, 0) in the document, typically like when the parent is
                    // <body>.
                    // If we ever allow other types of parent, .tooltipster-ruler
                    // will have to be appended to the parent to inherit css style
                    // values that affect the display of the text and such.
                    originParentOffset = {
                        left: helper.geo.origin.windowOffset.left + helper.geo.window.scroll.left,
                        top: helper.geo.origin.windowOffset.top + helper.geo.window.scroll.top
                    };
                }

                finalResult.coord = {
                    left: originParentOffset.left + (finalResult.coord.left - helper.geo.origin.windowOffset.left),
                    top: originParentOffset.top + (finalResult.coord.top - helper.geo.origin.windowOffset.top)
                };

                // set position values on the original tooltip element

                self.__sideChange(self.__instance._$tooltip, finalResult.side);

                if (helper.geo.origin.fixedLineage) {
                    self.__instance._$tooltip
                            .css('position', 'fixed');
                } else {
                    // CSS default
                    self.__instance._$tooltip
                            .css('position', '');
                }

                self.__instance._$tooltip
                        .css({
                            left: finalResult.coord.left,
                            top: finalResult.coord.top,
                            // we need to set a size even if the tooltip is in its natural size
                            // because when the tooltip is positioned beyond the width of the body
                            // (which is by default the width of the window; it will happen when
                            // you scroll the window horizontally to get to the origin), its text
                            // content will otherwise break lines at each word to keep up with the
                            // body overflow strategy.
                            height: finalResult.size.height,
                            width: finalResult.size.width
                        })
                        .find('.tooltipster-arrow')
                        .css({
                            'left': '',
                            'top': ''
                        })
                        .css(arrowCoord.prop, arrowCoord.val);

                // append the tooltip HTML element to its parent
                self.__instance._$tooltip.appendTo(self.__instance.option('parent'));

                self.__instance._trigger({
                    type: 'repositioned',
                    event: event,
                    position: finalResult
                });
            },

            /**
             * Make whatever modifications are needed when the side is changed. This has
             * been made an independant method for easy inheritance in custom plugins based
             * on this default plugin.
             *
             * @param {object} $obj
             * @param {string} side
             * @private
             */
            __sideChange: function ($obj, side) {

                $obj
                        .removeClass('tooltipster-bottom')
                        .removeClass('tooltipster-left')
                        .removeClass('tooltipster-right')
                        .removeClass('tooltipster-top')
                        .addClass('tooltipster-' + side);
            },

            /**
             * Returns the target that the tooltip should aim at for a given side.
             * The calculated value is a distance from the edge of the window
             * (left edge for top/bottom sides, top edge for left/right side). The
             * tooltip will be centered on that position and the arrow will be
             * positioned there (as much as possible).
             *
             * @param {object} helper
             * @return {integer}
             * @private
             */
            __targetFind: function (helper) {

                var target = {},
                        rects = this.__instance._$origin[0].getClientRects();

                // these lines fix a Chrome bug (issue #491)
                if (rects.length > 1) {
                    var opacity = this.__instance._$origin.css('opacity');
                    if (opacity == 1) {
                        this.__instance._$origin.css('opacity', 0.99);
                        rects = this.__instance._$origin[0].getClientRects();
                        this.__instance._$origin.css('opacity', 1);
                    }
                }

                // by default, the target will be the middle of the origin
                if (rects.length < 2) {

                    target.top = Math.floor(helper.geo.origin.windowOffset.left + (helper.geo.origin.size.width / 2));
                    target.bottom = target.top;

                    target.left = Math.floor(helper.geo.origin.windowOffset.top + (helper.geo.origin.size.height / 2));
                    target.right = target.left;
                }
                // if multiple client rects exist, the element may be text split
                // up into multiple lines and the middle of the origin may not be
                // best option anymore. We need to choose the best target client rect
                else {

                    // top: the first
                    var targetRect = rects[0];
                    target.top = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2);

                    // right: the middle line, rounded down in case there is an even
                    // number of lines (looks more centered => check out the
                    // demo with 4 split lines)
                    if (rects.length > 2) {
                        targetRect = rects[Math.ceil(rects.length / 2) - 1];
                    } else {
                        targetRect = rects[0];
                    }
                    target.right = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2);

                    // bottom: the last
                    targetRect = rects[rects.length - 1];
                    target.bottom = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2);

                    // left: the middle line, rounded up
                    if (rects.length > 2) {
                        targetRect = rects[Math.ceil((rects.length + 1) / 2) - 1];
                    } else {
                        targetRect = rects[rects.length - 1];
                    }

                    target.left = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2);
                }

                return target;
            }
        }
    });

    /* a build task will add "return $;" here */
    return $;

}));