/*!
 * Tipped - A Complete Javascript Tooltip Solution - v4.7.1
 * (c) 2012-2021 Nick Stakenburg
 *
 * http://www.tippedjs.com
 *
 * @license: https://creativecommons.org/licenses/by/4.0
 */

// UMD wrapper
(function(root, factory) {
  if (typeof define === 'function' && define.amd) {
    // AMD. Register as an anonymous module.
    define(['jquery'], factory);
  } else if (typeof module === 'object' && module.exports) {
    // Node/CommonJS
    module.exports = factory(require('jquery'));
  } else {
    // Browser globals
    root.Tipped = factory(jQuery);
  }
}(this, function($) {

var Tipped = {};

$.extend(Tipped, {
  version: '4.7.1'
});

Tipped.Skins = {
  // base skin, don't modify! (create custom skins in a separate file)
  base: {
    afterUpdate: false,
    ajax: {},
    cache: true,
    container: false,
    containment: {
      selector: "viewport",
      padding: 5
    },
    close: false,
    detach: true,
    fadeIn: 200,
    fadeOut: 200,
    showDelay: 75,
    hideDelay: 25,
    hideAfter: false,
    hideOn: { element: "mouseleave" },
    hideOthers: false,
    position: "top",
    inline: false,
    offset: { x: 0, y: 0 },
    onHide: false,
    onShow: false,
    padding: true,
    radius: true,
    shadow: true,
    showOn: { element: "mousemove" },
    size: "medium",
    spinner: true,
    stem: true,
    target: "element",
    voila: true
  },

  // Every other skin inherits from this one
  reset: {
    ajax: false,
    hideOn: {
      element: "mouseleave",
      tooltip: "mouseleave"
    },
    showOn: {
      element: "mouseenter",
      tooltip: "mouseenter"
    }
  }
};

$.each(
  "dark light gray red green blue lightyellow lightblue lightpink".split(" "),
  function(i, s) {
    Tipped.Skins[s] = {};
  }
);

var Browser = (function(uA) {
  function getVersion(identifier) {
    var version = new RegExp(identifier + "([\\d.]+)").exec(uA);
    return version ? parseFloat(version[1]) : true;
  }

  return {
    IE:
      !!(window.attachEvent && uA.indexOf("Opera") === -1) &&
      getVersion("MSIE "),
    Opera:
      uA.indexOf("Opera") > -1 &&
      ((!!window.opera && opera.version && parseFloat(opera.version())) ||
        7.55),
    WebKit: uA.indexOf("AppleWebKit/") > -1 && getVersion("AppleWebKit/"),
    Gecko:
      uA.indexOf("Gecko") > -1 &&
      uA.indexOf("KHTML") === -1 &&
      getVersion("rv:"),
    MobileSafari: !!uA.match(/Apple.*Mobile.*Safari/),
    Chrome: uA.indexOf("Chrome") > -1 && getVersion("Chrome/"),
    ChromeMobile: uA.indexOf("CrMo") > -1 && getVersion("CrMo/"),
    Android: uA.indexOf("Android") > -1 && getVersion("Android "),
    IEMobile: uA.indexOf("IEMobile") > -1 && getVersion("IEMobile/")
  };
})(navigator.userAgent);

var Support = (function() {
  var testElement = document.createElement("div"),
    domPrefixes = "Webkit Moz O ms Khtml".split(" ");

  function prefixed(property) {
    return testAllProperties(property, "prefix");
  }

  function testProperties(properties, prefixed) {
    for (var i in properties) {
      if (testElement.style[properties[i]] !== undefined) {
        return prefixed === "prefix" ? properties[i] : true;
      }
    }
    return false;
  }

  function testAllProperties(property, prefixed) {
    var ucProperty = property.charAt(0).toUpperCase() + property.substr(1),
      properties = (
        property +
        " " +
        domPrefixes.join(ucProperty + " ") +
        ucProperty
      ).split(" ");

    return testProperties(properties, prefixed);
  }

  // feature detect
  return {
    css: {
      animation: testAllProperties("animation"),
      transform: testAllProperties("transform"),
      prefixed: prefixed
    },

    shadow:
      testAllProperties("boxShadow") && testAllProperties("pointerEvents"),

    touch: (function() {
      try {
        return !!(
          "ontouchstart" in window ||
          (window.DocumentTouch && document instanceof DocumentTouch)
        ); // firefox for Android;
      } catch (e) {
        return false;
      }
    })()
  };
})();

var _slice = Array.prototype.slice;

var _ = {
  wrap: function(fn, wrapper) {
    var __fn = fn;
    return function() {
      var args = [$.proxy(__fn, this)].concat(_slice.call(arguments));
      return wrapper.apply(this, args);
    };
  },

  // is
  isElement: function(object) {
    return object && object.nodeType === 1;
  },

  isText: function(object) {
    return object && object.nodeType === 3;
  },

  isDocumentFragment: function(object) {
    return object && object.nodeType === 11;
  },

  delay: function(fn, ms) {
    var args = _slice.call(arguments, 2);
    return setTimeout(function() {
      return fn.apply(fn, args);
    }, ms);
  },

  defer: function(fn) {
    return _.delay.apply(this, [fn, 1].concat(_slice.call(arguments, 1)));
  },

  // Event
  pointer: function(event) {
    return { x: event.pageX, y: event.pageY };
  },

  element: {
    isAttached: (function() {
      function findTopAncestor(element) {
        // Walk up the DOM tree until we are at the top
        var ancestor = element;
        while (ancestor && ancestor.parentNode) {
          ancestor = ancestor.parentNode;
        }
        return ancestor;
      }

      return function(element) {
        var topAncestor = findTopAncestor(element);
        return !!(topAncestor && topAncestor.body);
      };
    })()
  }
};

function degrees(radian) {
  return (radian * 180) / Math.PI;
}

function radian(degrees) {
  return (degrees * Math.PI) / 180;
}

function sec(x) {
  return 1 / Math.cos(x);
}

function sfcc(c) {
  return String.fromCharCode.apply(String, c.replace(" ", "").split(","));
}

//deep extend
function deepExtend(destination, source) {
  for (var property in source) {
    if (
      source[property] &&
      source[property].constructor &&
      source[property].constructor === Object
    ) {
      destination[property] = $.extend({}, destination[property]) || {};
      deepExtend(destination[property], source[property]);
    } else {
      destination[property] = source[property];
    }
  }
  return destination;
}

var getUID = (function() {
  var count = 0,
    _prefix = "_tipped-uid-";

  return function(prefix) {
    prefix = prefix || _prefix;

    count++;
    // raise the count as long as we find a conflicting element on the page
    while (document.getElementById(prefix + count)) {
      count++;
    }
    return prefix + count;
  };
})();

var Position = {
  positions: [
    "topleft",
    "topmiddle",
    "topright",
    "righttop",
    "rightmiddle",
    "rightbottom",
    "bottomright",
    "bottommiddle",
    "bottomleft",
    "leftbottom",
    "leftmiddle",
    "lefttop"
  ],

  regex: {
    toOrientation: /^(top|left|bottom|right)(top|left|bottom|right|middle|center)$/,
    horizontal: /^(top|bottom)/,
    isCenter: /(middle|center)/,
    side: /^(top|bottom|left|right)/
  },

  toDimension: (function() {
    var translate = {
      top: "height",
      left: "width",
      bottom: "height",
      right: "width"
    };

    return function(position) {
      return translate[position];
    };
  })(),

  isCenter: function(position) {
    return !!position.toLowerCase().match(this.regex.isCenter);
  },

  isCorner: function(position) {
    return !this.isCenter(position);
  },

  //returns 'horizontal' or 'vertical' based on the options object
  getOrientation: function(position) {
    return position.toLowerCase().match(this.regex.horizontal)
      ? "horizontal"
      : "vertical";
  },

  getSide: function(position) {
    var side = null,
      matches = position.toLowerCase().match(this.regex.side);
    if (matches && matches[1]) side = matches[1];
    return side;
  },

  split: function(position) {
    return position.toLowerCase().match(this.regex.toOrientation);
  },

  _flip: {
    top: "bottom",
    bottom: "top",
    left: "right",
    right: "left"
  },
  flip: function(position, corner) {
    var split = this.split(position);

    if (corner) {
      return this.inverseCornerPlane(
        this.flip(this.inverseCornerPlane(position))
      );
    } else {
      return this._flip[split[1]] + split[2];
    }
  },

  inverseCornerPlane: function(position) {
    if (Position.isCorner(position)) {
      var split = this.split(position);
      return split[2] + split[1];
    } else {
      return position;
    }
  },

  adjustOffsetBasedOnPosition: function(
    offset,
    defaultTargetPosition,
    targetPosition
  ) {
    var adjustedOffset = $.extend({}, offset);
    var orientationXY = { horizontal: "x", vertical: "y" };
    var inverseXY = { x: "y", y: "x" };

    var inverseSides = {
      top: { right: "x" },
      bottom: { left: "x" },
      left: { bottom: "y" },
      right: { top: "y" }
    };

    var defaultOrientation = Position.getOrientation(defaultTargetPosition);
    if (defaultOrientation === Position.getOrientation(targetPosition)) {
      // we're on the same orientation
      // inverse when needed
      if (
        Position.getSide(defaultTargetPosition) !==
        Position.getSide(targetPosition)
      ) {
        var inverse = inverseXY[orientationXY[defaultOrientation]];
        adjustedOffset[inverse] *= -1;
      }
    } else {
      // moving to a side
      // flipXY
      var fx = adjustedOffset.x;
      adjustedOffset.x = adjustedOffset.y;
      adjustedOffset.y = fx;

      //  inversing x or y might be required based on movement
      var inverseSide =
        inverseSides[Position.getSide(defaultTargetPosition)][
          Position.getSide(targetPosition)
        ];
      if (inverseSide) {
        adjustedOffset[inverseSide] *= -1;
      }

      // nullify x or y
      // move to left/right (vertical) = nullify y
      adjustedOffset[
        orientationXY[Position.getOrientation(targetPosition)]
      ] = 0;
    }

    return adjustedOffset;
  },

  getBoxFromPoints: function(x1, y1, x2, y2) {
    var minX = Math.min(x1, x2),
      maxX = Math.max(x1, x2),
      minY = Math.min(y1, y2),
      maxY = Math.max(y1, y2);

    return {
      left: minX,
      top: minY,
      width: Math.max(maxX - minX, 0),
      height: Math.max(maxY - minY, 0)
    };
  },

  isPointWithinBox: function(x1, y1, bx1, by1, bx2, by2) {
    var box = this.getBoxFromPoints(bx1, by1, bx2, by2);

    return (
      x1 >= box.left &&
      x1 <= box.left + box.width &&
      y1 >= box.top &&
      y1 <= box.top + box.height
    );
  },
  isPointWithinBoxLayout: function(x, y, layout) {
    return this.isPointWithinBox(
      x,
      y,
      layout.position.left,
      layout.position.top,
      layout.position.left + layout.dimensions.width,
      layout.position.top + layout.dimensions.height
    );
  },

  getDistance: function(x1, y1, x2, y2) {
    return Math.sqrt(
      Math.pow(Math.abs(x2 - x1), 2) + Math.pow(Math.abs(y2 - y1), 2)
    );
  },

  intersectsLine: (function() {
    var ccw = function(x1, y1, x2, y2, x3, y3) {
      var cw = (y3 - y1) * (x2 - x1) - (y2 - y1) * (x3 - x1);
      return cw > 0 ? true : cw < 0 ? false : true;
    };

    return function(x1, y1, x2, y2, x3, y3, x4, y4, isReturnPosition) {
      if (!isReturnPosition) {
        /* http://www.bryceboe.com/2006/10/23/line-segment-intersection-algorithm/comment-page-1/ */
        return (
          ccw(x1, y1, x3, y3, x4, y4) != ccw(x2, y2, x3, y3, x4, y4) &&
          ccw(x1, y1, x2, y2, x3, y3) != ccw(x1, y1, x2, y2, x4, y4)
        );
      }

      /* http://stackoverflow.com/questions/563198/how-do-you-detect-where-two-line-segments-intersect/1968345#1968345 */
      var s1_x, s1_y, s2_x, s2_y;
      s1_x = x2 - x1;
      s1_y = y2 - y1;
      s2_x = x4 - x3;
      s2_y = y4 - y3;

      var s, t;
      s = (-s1_y * (x1 - x3) + s1_x * (y1 - y3)) / (-s2_x * s1_y + s1_x * s2_y);
      t = (s2_x * (y1 - y3) - s2_y * (x1 - x3)) / (-s2_x * s1_y + s1_x * s2_y);

      if (s >= 0 && s <= 1 && t >= 0 && t <= 1) {
        // Collision detected
        var atX = x1 + t * s1_x;
        var atY = y1 + t * s1_y;
        return { x: atX, y: atY };
      }

      return false; // No collision
    };
  })()
};

var Bounds = {
  viewport: function() {
    var vp;
    if (Browser.MobileSafari || (Browser.Android && Browser.Gecko)) {
      vp = { width: window.innerWidth, height: window.innerHeight };
    } else {
      vp = {
        height: $(window).height(),
        width: $(window).width()
      };
    }

    return vp;
  }
};

var Mouse = {
  _buffer: { pageX: 0, pageY: 0 },
  _dimensions: {
    width: 30, // should both be even
    height: 30
  },
  _shift: {
    x: 2,
    y: 10 // correction so the tooltip doesn't appear on top of the mouse
  },

  // a modified version of the actual position, to match the box
  getPosition: function(event) {
    var position = this.getActualPosition(event);

    return {
      left:
        position.left -
        Math.round(this._dimensions.width * 0.5) +
        this._shift.x,
      top:
        position.top - Math.round(this._dimensions.height * 0.5) + this._shift.y
    };
  },

  getActualPosition: function(event) {
    var position =
      event && typeof event.pageX === "number" ? event : this._buffer;

    return {
      left: position.pageX,
      top: position.pageY
    };
  },

  getDimensions: function() {
    return this._dimensions;
  }
};

var Color = (function() {
  var names = {
    _default: "#000000",
    aqua: "#00ffff",
    black: "#000000",
    blue: "#0000ff",
    fuchsia: "#ff00ff",
    gray: "#808080",
    green: "#008000",
    lime: "#00ff00",
    maroon: "#800000",
    navy: "#000080",
    olive: "#808000",
    purple: "#800080",
    red: "#ff0000",
    silver: "#c0c0c0",
    teal: "#008080",
    white: "#ffffff",
    yellow: "#ffff00"
  };

  function hex(x) {
    return ("0" + parseInt(x).toString(16)).slice(-2);
  }

  function rgb2hex(rgb) {
    rgb = rgb.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+))?\)$/);
    return "#" + hex(rgb[1]) + hex(rgb[2]) + hex(rgb[3]);
  }

  return {
    toRGB: function(str) {
      if (/^rgba?\(/.test(str)) {
        return rgb2hex(str);
      } else {
        // first try color name to hex
        if (names[str]) str = names[str];

        // assume already hex, just normalize #rgb #rrggbb
        var hex = str.replace("#", "");
        if (!/^(?:[0-9a-fA-F]{3}){1,2}$/.test(hex)) return names._default;

        if (hex.length == 3) {
          hex =
            hex.charAt(0) +
            hex.charAt(0) +
            hex.charAt(1) +
            hex.charAt(1) +
            hex.charAt(2) +
            hex.charAt(2);
        }

        return "#" + hex;
      }
    }
  };
})();

// Spin
// Create pure CSS based spinners
function Spin() {
  return this.initialize.apply(this, _slice.call(arguments));
}

// mark as supported
Spin.supported = Support.css.transform && Support.css.animation;

$.extend(Spin.prototype, {
  initialize: function() {
    this.options = $.extend({}, arguments[0] || {});

    this.build();
    this.start();
  },

  build: function() {
    var d = (this.options.length + this.options.radius) * 2;
    var dimensions = { height: d, width: d };

    this.element = $("<div>")
      .addClass("tpd-spin")
      .css(dimensions);

    this.element.append(
      (this._rotate = $("<div>").addClass("tpd-spin-rotate"))
    );

    this.element.css({
      "margin-left": -0.5 * dimensions.width,
      "margin-top": -0.5 * dimensions.height
    });

    var lines = this.options.lines;

    // insert 12 frames
    for (var i = 0; i < lines; i++) {
      var frame, line;
      this._rotate.append(
        (frame = $("<div>")
          .addClass("tpd-spin-frame")
          .append((line = $("<div>").addClass("tpd-spin-line"))))
      );

      line.css({
        "background-color": this.options.color,
        width: this.options.width,
        height: this.options.length,
        "margin-left": -0.5 * this.options.width,
        "border-radius": Math.round(0.5 * this.options.width)
      });

      frame.css({ opacity: ((1 / lines) * (i + 1)).toFixed(2) });

      var transformCSS = {};
      transformCSS[Support.css.prefixed("transform")] =
        "rotate(" + (360 / lines) * (i + 1) + "deg)";
      frame.css(transformCSS);
    }
  },

  start: function() {
    var rotateCSS = {};
    rotateCSS[Support.css.prefixed("animation")] =
      "tpd-spin 1s infinite steps(" + this.options.lines + ")";
    this._rotate.css(rotateCSS);
  },

  stop: function() {
    var rotateCSS = {};
    rotateCSS[Support.css.prefixed("animation")] = "none";
    this._rotate.css(rotateCSS);
    this.element.detach();
  }
});

function Visible() {
  return this.initialize.apply(this, _slice.call(arguments));
}

$.extend(Visible.prototype, {
  initialize: function(elements) {
    elements = elements instanceof Array ? elements : [elements]; // ensure array
    this.elements = elements;

    this._restore = [];
    $.each(elements, $.proxy(function(i, element) {
      var visible = $(element).is(':visible');

      if (!visible) {
        $(element).show();
      }

      this._restore.push({
        element: element,
        visible: visible
      });

    }, this));
    return this;
  },

  restore: function() {
    $.each(this._restore, function(i, entry) {
      if (!entry.visible) {
        $(entry.element).show();
      }
    });

    this._restore = null;
  }
});

var AjaxCache = (function() {
  var cache = [];

  return {
    // return an update object to pass onto tooltip.update()
    get: function(ajax) {
      var entry = null;
      for (var i = 0; i < cache.length; i++) {
        if (
          cache[i] &&
          cache[i].url === ajax.url &&
          (cache[i].type || "GET").toUpperCase() ===
            (ajax.type || "GET").toUpperCase() &&
          $.param(cache[i].data || {}) === $.param(ajax.data || {})
        ) {
          entry = cache[i];
        }
      }
      return entry;
    },

    set: function(ajax, callbackName, args) {
      var entry = this.get(ajax);
      if (!entry) {
        entry = $.extend({ callbacks: {} }, ajax);
        cache.push(entry);
      }

      entry.callbacks[callbackName] = args;
    },

    remove: function(url) {
      for (var i = 0; i < cache.length; i++) {
        if (cache[i] && cache[i].url === url) {
          delete cache[i];
        }
      }
    },

    clear: function() {
      cache = [];
    }
  };
})();

/*!
 * Voilà - v1.3.0
 * (c) 2015 Nick Stakenburg
 *
 * http://voila.nickstakenburg.com
 *
 * MIT License
 */

var Voila = (function($) {
  function Voila(elements, opts, cb) {
    if (!(this instanceof Voila)) {
      return new Voila(elements, opts, cb);
    }

    var argTypeOne = typeof arguments[1],
      options = argTypeOne === "object" ? arguments[1] : {},
      callback =
        argTypeOne === "function"
          ? arguments[1]
          : typeof arguments[2] === "function"
          ? arguments[2]
          : false;

    this.options = $.extend(
      {
        method: "onload"
      },
      options
    );

    this.deferred = new jQuery.Deferred();

    // if there's a callback, push it onto the stack
    if (callback) {
      this.always(callback);
    }

    this._processed = 0;
    this.images = [];
    this._add(elements);

    return this;
  }

  $.extend(Voila.prototype, {
    _add: function(elements) {
      // normalize to an array
      var array =
        typeof elements === "string"
          ? $(elements) // selector
          : elements instanceof jQuery || elements.length > 0
          ? elements // jQuery obj, Array
          : [elements]; // element node

      // subtract the images
      $.each(
        array,
        $.proxy(function(i, element) {
          var images = $(),
            $element = $(element);

          // single image
          if ($element.is("img")) {
            images = images.add($element);
          } else {
            // nested
            images = images.add($element.find("img"));
          }

          images.each(
            $.proxy(function(i, element) {
              this.images.push(
                new ImageReady(
                  element,
                  // success
                  $.proxy(function(image) {
                    this._progress(image);
                  }, this),
                  // error
                  $.proxy(function(image) {
                    this._progress(image);
                  }, this),
                  // options
                  this.options
                )
              );
            }, this)
          );
        }, this)
      );

      // no images found
      if (this.images.length < 1) {
        setTimeout(
          $.proxy(function() {
            this._resolve();
          }, this)
        );
      }
    },

    abort: function() {
      // clear callbacks
      this._progress = this._notify = this._reject = this._resolve = function() {};

      // clear images
      $.each(this.images, function(i, image) {
        image.abort();
      });
      this.images = [];
    },

    _progress: function(image) {
      this._processed++;

      // when a broken image passes by keep track of it
      if (!image.isLoaded) this._broken = true;

      this._notify(image);

      // completed
      if (this._processed === this.images.length) {
        this[this._broken ? "_reject" : "_resolve"]();
      }
    },

    _notify: function(image) {
      this.deferred.notify(this, image);
    },
    _reject: function() {
      this.deferred.reject(this);
    },
    _resolve: function() {
      this.deferred.resolve(this);
    },

    always: function(callback) {
      this.deferred.always(callback);
      return this;
    },

    done: function(callback) {
      this.deferred.done(callback);
      return this;
    },

    fail: function(callback) {
      this.deferred.fail(callback);
      return this;
    },

    progress: function(callback) {
      this.deferred.progress(callback);
      return this;
    }
  });

  /* ImageReady (standalone) - part of Voilà
   * http://voila.nickstakenburg.com
   * MIT License
   */
  var ImageReady = (function($) {
    var Poll = function() {
      return this.initialize.apply(this, Array.prototype.slice.call(arguments));
    };
    $.extend(Poll.prototype, {
      initialize: function() {
        this.options = $.extend(
          {
            test: function() {},
            success: function() {},
            timeout: function() {},
            callAt: false,
            intervals: [
              [0, 0],
              [1 * 1000, 10],
              [2 * 1000, 50],
              [4 * 1000, 100],
              [20 * 1000, 500]
            ]
          },
          arguments[0] || {}
        );

        this._test = this.options.test;
        this._success = this.options.success;
        this._timeout = this.options.timeout;

        this._ipos = 0;
        this._time = 0;
        this._delay = this.options.intervals[this._ipos][1];
        this._callTimeouts = [];

        this.poll();
        this._createCallsAt();
      },

      poll: function() {
        this._polling = setTimeout(
          $.proxy(function() {
            if (this._test()) {
              this.success();
              return;
            }

            // update time
            this._time += this._delay;

            // next i within the interval
            if (this._time >= this.options.intervals[this._ipos][0]) {
              // timeout when no next interval
              if (!this.options.intervals[this._ipos + 1]) {
                if (typeof this._timeout === "function") {
                  this._timeout();
                }
                return;
              }

              this._ipos++;

              // update to the new bracket
              this._delay = this.options.intervals[this._ipos][1];
            }

            this.poll();
          }, this),
          this._delay
        );
      },

      success: function() {
        this.abort();
        this._success();
      },

      _createCallsAt: function() {
        if (!this.options.callAt) return;

        // start a timer for each call
        $.each(
          this.options.callAt,
          $.proxy(function(i, at) {
            var time = at[0],
              fn = at[1];

            var timeout = setTimeout(
              $.proxy(function() {
                fn();
              }, this),
              time
            );

            this._callTimeouts.push(timeout);
          }, this)
        );
      },

      _stopCallTimeouts: function() {
        $.each(this._callTimeouts, function(i, timeout) {
          clearTimeout(timeout);
        });
        this._callTimeouts = [];
      },

      abort: function() {
        this._stopCallTimeouts();

        if (this._polling) {
          clearTimeout(this._polling);
          this._polling = null;
        }
      }
    });

    var ImageReady = function() {
      return this.initialize.apply(this, Array.prototype.slice.call(arguments));
    };
    $.extend(ImageReady.prototype, {
      supports: {
        naturalWidth: (function() {
          return "naturalWidth" in new Image();
        })()
      },

      // NOTE: setTimeouts allow callbacks to be attached
      initialize: function(img, successCallback, errorCallback) {
        this.img = $(img)[0];
        this.successCallback = successCallback;
        this.errorCallback = errorCallback;
        this.isLoaded = false;

        this.options = $.extend(
          {
            method: "onload",
            pollFallbackAfter: 1000
          },
          arguments[3] || {}
        );

        // onload and a fallback for no naturalWidth support (IE6-7)
        if (this.options.method == "onload" || !this.supports.naturalWidth) {
          this.load();
          return;
        }

        // start polling
        this.poll();
      },

      // NOTE: Polling for naturalWidth is only reliable if the
      // <img>.src never changes. naturalWidth isn't always reset
      // to 0 after the src changes (depending on how the spec
      // was implemented). The spec even seems to be against
      // this, making polling unreliable in those cases.
      poll: function() {
        this._poll = new Poll({
          test: $.proxy(function() {
            return this.img.naturalWidth > 0;
          }, this),

          success: $.proxy(function() {
            this.success();
          }, this),

          timeout: $.proxy(function() {
            // error on timeout
            this.error();
          }, this),

          callAt: [
            [
              this.options.pollFallbackAfter,
              $.proxy(function() {
                this.load();
              }, this)
            ]
          ]
        });
      },

      load: function() {
        this._loading = setTimeout(
          $.proxy(function() {
            var image = new Image();
            this._onloadImage = image;

            image.onload = $.proxy(function() {
              image.onload = function() {};

              if (!this.supports.naturalWidth) {
                this.img.naturalWidth = image.width;
                this.img.naturalHeight = image.height;
                image.naturalWidth = image.width;
                image.naturalHeight = image.height;
              }

              this.success();
            }, this);

            image.onerror = $.proxy(this.error, this);

            image.src = this.img.src;
          }, this)
        );
      },

      success: function() {
        if (this._calledSuccess) return;

        this._calledSuccess = true;

        // stop loading/polling
        this.abort();

        // some time to allow layout updates, IE requires this!
        this.waitForRender(
          $.proxy(function() {
            this.isLoaded = true;
            this.successCallback(this);
          }, this)
        );
      },

      error: function() {
        if (this._calledError) return;

        this._calledError = true;

        // stop loading/polling
        this.abort();

        // don't wait for an actual render on error, just timeout
        // to give the browser some time to render a broken image icon
        this._errorRenderTimeout = setTimeout(
          $.proxy(function() {
            if (this.errorCallback) this.errorCallback(this);
          }, this)
        );
      },

      abort: function() {
        this.stopLoading();
        this.stopPolling();
        this.stopWaitingForRender();
      },

      stopPolling: function() {
        if (this._poll) {
          this._poll.abort();
          this._poll = null;
        }
      },

      stopLoading: function() {
        if (this._loading) {
          clearTimeout(this._loading);
          this._loading = null;
        }

        if (this._onloadImage) {
          this._onloadImage.onload = function() {};
          this._onloadImage.onerror = function() {};
        }
      },

      // used by success() only
      waitForRender: function(callback) {
        this._renderTimeout = setTimeout(callback);
      },

      stopWaitingForRender: function() {
        if (this._renderTimeout) {
          clearTimeout(this._renderTimeout);
          this._renderTimeout = null;
        }

        if (this._errorRenderTimeout) {
          clearTimeout(this._errorRenderTimeout);
          this._errorRenderTimeout = null;
        }
      }
    });

    return ImageReady;
  })(jQuery);

  return Voila;
})(jQuery);

Tipped.Behaviors = {
  hide: {
    showOn: {
      element: "mouseenter",
      tooltip: false
    },
    hideOn: {
      element: "mouseleave",
      tooltip: "mouseenter"
    }
  },

  mouse: {
    showOn: {
      element: "mouseenter",
      tooltip: false
    },
    hideOn: {
      element: "mouseleave",
      tooltip: "mouseenter"
    },
    target: "mouse",
    showDelay: 100,
    fadeIn: 0,
    hideDelay: 0,
    fadeOut: 0
  },

  sticky: {
    showOn: {
      element: "mouseenter",
      tooltip: "mouseenter"
    },
    hideOn: {
      element: "mouseleave",
      tooltip: "mouseleave"
    },
    // more show delay solves issues positioning at the initial mouse
    // position when elements span multiple lines/line-breaks, since
    // the mouse won't be positioning close to the edge
    showDelay: 150,
    target: "mouse",
    fixed: true
  }
};

var Options = {
  create: (function() {
    var BASE, RESET;

    // hideOn helper
    function toDisplayObject(input, display) {
      var on;
      if ( typeof input === "string") {
        on = {
          element:
            (RESET[display] && RESET[display].element) || BASE[display].element,
          event: input
        };
      } else {
        on = deepExtend($.extend({}, BASE[display]), input);
      }

      return on;
    }

    // hideOn helper
    function initialize(options) {
      BASE = Tipped.Skins.base;
      RESET = deepExtend($.extend({}, BASE), Tipped.Skins["reset"]);
      initialize = create;
      return create(options);
    }

    function middleize(position) {
      if (position.match(/^(top|left|bottom|right)$/)) {
        position += "middle";
      }

      position.replace("center", "middle").replace(" ", "");

      return position;
    }

    function presetiffy(options) {
      var preset, behavior;
      if (options.behavior && (behavior = Tipped.Behaviors[options.behavior])) {
        preset = deepExtend($.extend({}, behavior), options);
      } else {
        preset = options;
      }

      return preset;
    }

    function create(options) {
      var selected_skin = options.skin
        ? options.skin
        : Tooltips.options.defaultSkin;
      var SELECTED = $.extend({}, Tipped.Skins[selected_skin] || {});
      // make sure the skin option is set
      if (!SELECTED.skin) {
        SELECTED.skin = Tooltips.options.defaultSkin || "dark";
      }

      var MERGED_SELECTED = deepExtend(
        $.extend({}, RESET),
        presetiffy(SELECTED)
      ); // work presets into selected skin

      var MERGED = deepExtend(
        $.extend({}, MERGED_SELECTED),
        presetiffy(options)
      ); // also work presets into the given options

      // Ajax
      if (MERGED.ajax) {
        var RESET_ajax = RESET.ajax || {},
          BASE_ajax = BASE.ajax;

        if ( typeof MERGED.ajax === "boolean") {
          // true
          MERGED.ajax = {
            //method: RESET_ajax.type || BASE_ajax.type
          };
        }
        // otherwise it must be an object
        MERGED.ajax = deepExtend($.extend({}, BASE_ajax), MERGED.ajax);
      }

      var position;
      var targetPosition = (targetPosition =
        (MERGED.position && MERGED.position.target) ||
        ( typeof MERGED.position === "string" && MERGED.position) ||
        (RESET.position && RESET.position.target) ||
        ( typeof RESET.position === "string" && RESET.position) ||
        (BASE.position && BASE.position.target) ||
        BASE.position);
      targetPosition = middleize(targetPosition);

      var tooltipPosition =
        (MERGED.position && MERGED.position.tooltip) ||
        (RESET.position && RESET.position.tooltip) ||
        (BASE.position && BASE.position.tooltip) ||
        Tooltips.Position.getInversedPosition(targetPosition);
      tooltipPosition = middleize(tooltipPosition);

      if (MERGED.position) {
        if (typeof MERGED.position === "string") {
          MERGED.position = middleize(MERGED.position);
          position = {
            target: MERGED.position,
            tooltip: Tooltips.Position.getTooltipPositionFromTarget(
              MERGED.position
            )
          };
        } else {
          // object
          position = { tooltip: tooltipPosition, target: targetPosition };
          if (MERGED.position.tooltip) {
            position.tooltip = middleize(MERGED.position.tooltip);
          }
          if (MERGED.position.target) {
            position.target = middleize(MERGED.position.target);
          }
        }
      } else {
        position = {
          tooltip: tooltipPosition,
          target: targetPosition
        };
      }

      // make sure the 2 positions are on the same plane when centered
      // this aligns the sweet spot when repositioning based on the stem
      if (
        Position.isCorner(position.target) &&
        Position.getOrientation(position.target) !==
          Position.getOrientation(position.tooltip)
      ) {
        // switch over the target only, cause we shouldn't be resetting the stem on the tooltip
        position.target = Position.inverseCornerPlane(position.target);
      }

      // if we're hooking to the mouse we want the center
      if (MERGED.target === "mouse") {
        var orientation = Position.getOrientation(position.target);

        // force center alignment on the mouse
        if (orientation === "horizontal") {
          position.target = position.target.replace(/(left|right)/, "middle");
        } else {
          position.target = position.target.replace(/(top|bottom)/, "middle");
        }
      }

      // if the target is the mouse we set the position to 'bottomright' so the position system can work with it
      MERGED.position = position;

      // Offset
      var offset;
      if (MERGED.target === "mouse") {
        // get the offset of the base class
        offset = $.extend({}, BASE.offset);
        $.extend(offset, Tipped.Skins["reset"].offset || {});

        if (options.skin) {
          $.extend(
            offset,
            (
              Tipped.Skins[options.skin] ||
              Tipped.Skins[Tooltips.options.defaultSkin] ||
              {}
            ).offset || {}
          );
        }

        // find out what the offset should be
        offset = Position.adjustOffsetBasedOnPosition(
          BASE.offset,
          BASE.position,
          position.target,
          true
        );

        // now put any given options on top of that
        if (options.offset) {
          offset = $.extend(offset, options.offset || {});
        }
      } else {
        offset = {
          x: MERGED.offset.x,
          y: MERGED.offset.y
        };
      }

      MERGED.offset = offset;

      // hideOnClickOutside
      if (MERGED.hideOn && MERGED.hideOn === "click-outside") {
        MERGED.hideOnClickOutside = true;
        MERGED.hideOn = false;
        MERGED.fadeOut = 0; // instantly fadeout for better UI
      }

      if (MERGED.showOn) {
        // showOn and hideOn should not abide by inheritance,
        // otherwise we'd always have the BASE/RESET object for it as starting point
        var showOn = MERGED.showOn;

        if ( typeof showOn === "string") {
          showOn = { element: showOn };
        }

        MERGED.showOn = showOn;
      }

      if (MERGED.hideOn) {
        var hideOn = MERGED.hideOn;

        if (typeof hideOn === "string") {
          hideOn = { element: hideOn };
        }

        MERGED.hideOn = hideOn;
      }

      // normalize inline
      if (MERGED.inline) {
        if (typeof MERGED.inline !== "string") {
          MERGED.inline = false;
        }
      }

      // fadeIn 0 on IE < 9 to prevent text transform during fade
      if (Browser.IE && Browser.IE < 9) {
        $.extend(MERGED, { fadeIn: 0, fadeOut: 0, hideDelay: 0 });
      }

      if (MERGED.spinner) {
        if (!Spin.supported) {
          MERGED.spinner = false;
        } else {
          if (typeof MERGED.spinner === "boolean") {
            MERGED.spinner = RESET.spinner || BASE.spinner || {};
          }
        }
      }

      if (!MERGED.container) {
        MERGED.container = document.body;
      }

      if (MERGED.containment) {
        if (typeof MERGED.containment === "string") {
          MERGED.containment = {
            selector: MERGED.containment,
            padding:
              (RESET.containment && RESET.containment.padding) ||
              (BASE.padding && BASE.containment.padding)
          };
        }
      }

      // normalize shadow, setting it to true should only mean it's enabled when supported
      if (MERGED.shadow) {
        MERGED.shadow = Support.shadow;
      }

      return MERGED;
    }

    return initialize;
  })()
};

function Skin() {
  this.initialize.apply(this, _slice.call(arguments));
}

$.extend(Skin.prototype, {
  initialize: function(tooltip) {
    this.tooltip = tooltip;
    this.element = tooltip._skin;

    // classes to further style the tooltip
    var options = this.tooltip.options;
    this.tooltip._tooltip[(options.shadow ? "remove" : "add") + "Class"](
      "tpd-no-shadow"
    )
      [(options.radius ? "remove" : "add") + "Class"]("tpd-no-radius")
      [(options.stem ? "remove" : "add") + "Class"]("tpd-no-stem");

    // we should get radius and border when initializing
    var frames, bg, bgc, spinner;
    var prefixedRadius = Support.css.prefixed("borderTopLeftRadius");

    this.element
      .append(
        (frames = $("<div>")
          .addClass("tpd-frames")
          .append(
            $("<div>")
              .addClass("tpd-frame")
              .append(
                $("<div>")
                  .addClass("tpd-backgrounds")
                  .append(
                    (bg = $("<div>")
                      .addClass("tpd-background")
                      .append(
                        (bgc = $("<div>").addClass("tpd-background-content"))
                      ))
                  )
              )
          ))
      )
      .append((spinner = $("<div>").addClass("tpd-spinner")));

    // REQUIRED FOR IE < 8
    bg.css({ width: 999, height: 999, zoom: 1 });

    this._css = {
      border: parseFloat(bg.css("border-top-width")),
      radius: parseFloat(prefixedRadius ? bg.css(prefixedRadius) : 0),
      padding: parseFloat(tooltip._content.css("padding-top")),
      borderColor: bg.css("border-top-color"),
      //spacing: parseFloat(this.element.css('margin-top')),
      // borderOpacity: .5 // IE pre rgba fallback can be inserted here
      backgroundColor: bgc.css("background-color"),
      backgroundOpacity: bgc.css("opacity"),
      spinner: {
        dimensions: {
          width: spinner.innerWidth(),
          height: spinner.innerHeight()
        }
      }
    };

    spinner.remove();
    frames.remove();

    this._side = Position.getSide(tooltip.options.position.tooltip) || "top";

    this._vars = {};
  },

  destroy: function() {
    if (!this.frames) return;

    // remove all the stems
    $.each(
      "top right bottom left".split(" "),
      $.proxy(function(i, side) {
        if (this["stem_" + side]) this["stem_" + side].destroy();
      }, this)
    );

    this.frames.remove();
    this.frames = null;
  },

  build: function() {
    // if already build exit
    if (this.frames) return;

    this.element.append((this.frames = $("<div>").addClass("tpd-frames")));

    $.each(
      "top right bottom left".split(" "),
      $.proxy(function(i, side) {
        this.insertFrame(side);
      }, this)
    );

    // insert a spinner, if we haven't already
    if (!this._spinner) {
      this.tooltip._tooltip.append(
        (this._spinner = $("<div>")
          .addClass("tpd-spinner")
          .hide()
          .append($("<div>").addClass("tpd-spinner-spin")))
      );
    }
  },

  _frame: (function() {
    var backgrounds;

    var frame = $("<div>")
      .addClass("tpd-frame")
      // background
      .append(
        (backgrounds = $("<div>")
          .addClass("tpd-backgrounds")
          .append($("<div>").addClass("tpd-background-shadow")))
      )
      .append(
        $("<div>")
          .addClass("tpd-shift-stem")
          .append(
            $("<div>").addClass(
              "tpd-shift-stem-side tpd-shift-stem-side-before"
            )
          )
          .append($("<div>").addClass("tpd-stem"))
          .append(
            $("<div>").addClass("tpd-shift-stem-side tpd-shift-stem-side-after")
          )
      );

    $.each(
      "top right bottom left".split(" "),
      $.proxy(function(i, s) {
        backgrounds.append(
          $("<div>")
            .addClass("tpd-background-box tpd-background-box-" + s)
            .append(
              $("<div>")
                .addClass("tpd-background-box-shift")
                .append(
                  $("<div>")
                    .addClass("tpd-background-box-shift-further")
                    .append(
                      $("<div>")
                        .addClass("tpd-background")
                        .append($("<div>").addClass("tpd-background-title"))
                        .append($("<div>").addClass("tpd-background-content"))
                    )
                    .append(
                      $("<div>").addClass(
                        "tpd-background tpd-background-loading"
                      )
                    )
                    .append(
                      $("<div>")
                        .addClass("tpd-background-border-hack")
                        .hide()
                    )
                )
            )
        );
      }, this)
    );

    return frame;
  })(),

  _getFrame: function(side) {
    var frame = this._frame.clone();

    // class
    frame.addClass("tpd-frame-" + side);

    // put border radius on shadow
    frame
      .find(".tpd-background-shadow")
      .css({ "border-radius": this._css.radius });

    // mark side on stem
    if (this.tooltip.options.stem) {
      frame.find(".tpd-stem").attr("data-stem-position", side);
    }

    // radius on background layers
    var innerBackgroundRadius = Math.max(
      this._css.radius - this._css.border,
      0
    );
    frame.find(".tpd-background-title").css({
      "border-top-left-radius": innerBackgroundRadius,
      "border-top-right-radius": innerBackgroundRadius
    });
    frame.find(".tpd-background-content").css({
      "border-bottom-left-radius": innerBackgroundRadius,
      "border-bottom-right-radius": innerBackgroundRadius
    });
    frame.find(".tpd-background-loading").css({
      "border-radius": innerBackgroundRadius
    });

    // adjust the dimensions of the shift sides
    var ss = { backgroundColor: this._css.borderColor };
    var orientation = Position.getOrientation(side),
      isHorizontal = orientation === "horizontal";
    ss[isHorizontal ? "height" : "width"] = this._css.border + "px";
    var inverse = {
      top: "bottom",
      bottom: "top",
      left: "right",
      right: "left"
    };
    ss[inverse[side]] = 0;
    frame.find(".tpd-shift-stem-side").css(ss);

    return frame;
  },

  insertFrame: function(side) {
    var frame = (this["frame_" + side] = this._getFrame(side));
    this.frames.append(frame);

    if (this.tooltip.options.stem) {
      var stem = frame.find(".tpd-stem");
      this["stem_" + side] = new Stem(stem, this, {});
    }
  },

  // Loading
  startLoading: function() {
    if (!this.tooltip.supportsLoading) return;
    this.build(); // make sure the tooltip is build

    // resize to the dimensions of the spinner the first time a tooltip is shown
    if (!this._spinner && !this.tooltip.is("resize-to-content")) {
      this.setDimensions(this._css.spinner.dimensions); // this creates ._spinner
    }

    if (this._spinner) {
      this._spinner.show();
    }
  },

  // the idea behind stopLoading is that dimensions are set right after calling this function
  // that's why we don't set the manually here
  stopLoading: function() {
    if (!this.tooltip.supportsLoading || !this._spinner) return;
    this.build(); // make sure the tooltip is build

    this._spinner.hide();
  },

  // updates the background of the currently active side
  updateBackground: function() {
    var frame = this._vars.frames[this._side];

    var backgroundDimensions = $.extend({}, frame.background.dimensions);

    if (this.tooltip.title && !this.tooltip.is("loading")) {
      // show both background children
      this.element
        .find(".tpd-background-title, .tpd-background-content")
        .show();

      // remove background color and replace it with transparent
      this.element
        .find(".tpd-background")
        .css({ "background-color": "transparent" });

      var contentDimensions = $.extend({}, backgroundDimensions);
      var innerBackgroundRadius = Math.max(
        this._css.radius - this._css.border,
        0
      );
      var contentRadius = {
        "border-top-left-radius": innerBackgroundRadius,
        "border-top-right-radius": innerBackgroundRadius,
        "border-bottom-left-radius": innerBackgroundRadius,
        "border-bottom-right-radius": innerBackgroundRadius
      };

      // measure the title
      var visible = new Visible(this.tooltip._tooltip);

      var titleHeight = this.tooltip._titleWrapper.innerHeight(); // without margins

      contentDimensions.height -= titleHeight;

      // set all title dimensions
      this.element.find(".tpd-background-title").css({
        height: titleHeight,
        width: backgroundDimensions.width
      });

      // remove radius at the top
      contentRadius["border-top-left-radius"] = 0;
      contentRadius["border-top-right-radius"] = 0;

      visible.restore();

      // set all content dimensions
      // set correct radius
      this.element
        .find(".tpd-background-content")
        .css(contentDimensions)
        .css(contentRadius);

      // loading indicator
      this.element.find(".tpd-background-loading").css({
        "background-color": this._css.backgroundColor
      });
    } else {
      // no title or close button creates a bar at the top
      // set background color only for better px handling in the corners
      // show both background children
      this.element
        .find(".tpd-background-title, .tpd-background-content")
        .hide();

      this.element
        .find(".tpd-background")
        .css({ "background-color": this._css.backgroundColor });
    }

    // border fix, required as a workaround for the following bugs:
    // https://bugzilla.mozilla.org/show_bug.cgi?id=929979
    // https://code.google.com/p/chromium/issues/detail?id=320330
    if (this._css.border) {
      this.element
        .find(".tpd-background")
        .css({ "border-color": "transparent" });

      this.element
        .find(".tpd-background-border-hack")
        // scaled
        .css({
          width: backgroundDimensions.width,
          height: backgroundDimensions.height,
          "border-radius": this._css.radius,
          "border-width": this._css.border,
          "border-color": this._css.borderColor
        })
        .show();
    }
  },

  // update dimensions of the currently active side
  // background + stem
  paint: function() {
    // don't update if we've already rendered the dimensions at current stem position
    if (
      this._paintedDimensions &&
      this._paintedDimensions.width === this._dimensions.width &&
      this._paintedDimensions.height === this._dimensions.height &&
      this._paintedStemPosition === this._stemPosition
    ) {
      return;
    }

    // store these to prevent future updates at the same dimensions
    this._paintedDimensions = this._dimensions;
    this._paintedStemPosition = this._stemPosition;

    // visible side, hide others
    this.element
      .removeClass(
        "tpd-visible-frame-top tpd-visible-frame-bottom tpd-visible-frame-left tpd-visible-frame-right"
      )
      .addClass("tpd-visible-frame-" + this._side);

    var frame = this._vars.frames[this._side];

    // set dimensions
    var backgroundDimensions = $.extend({}, frame.background.dimensions);
    this.element.find(".tpd-background").css(backgroundDimensions);
    this.element.find(".tpd-background-shadow").css({
      width: backgroundDimensions.width + 2 * this._css.border,
      height: backgroundDimensions.height + 2 * this._css.border
    });

    // update background to the correct display method
    this.updateBackground();

    this.element
      .find(".tpd-background-box-shift, .tpd-background-box-shift-further")
      .removeAttr("style");

    // dimensions of the skin
    this.element
      .add(this.frames)
      // and the tooltip
      .add(this.tooltip._tooltip)
      .css(frame.dimensions);

    // resize every frame
    var name = this._side,
      value = this._vars.frames[name];
    var f = this.element.find(".tpd-frame-" + this._side),
      fdimensions = this._vars.frames[name].dimensions;

    f.css(fdimensions);

    // background
    f.find(".tpd-backgrounds").css(
      $.extend({}, value.background.position, {
        width: fdimensions.width - value.background.position.left,
        height: fdimensions.height - value.background.position.top
      })
    );

    // find the position of this frame
    // adjust the backgrounds
    var orientation = Position.getOrientation(name);

    // no stem only shows the top frame (using CSS)
    // with a stem we have to make adjustments
    if (this.tooltip.options.stem) {
      // position the shiftstem
      f.find(".tpd-shift-stem").css(
        $.extend({}, value.shift.dimensions, value.shift.position)
      );

      if (orientation === "vertical") {
        // left or right
        // top or bottom
        // make top/bottom side small
        var smallBoxes = f.find(
          ".tpd-background-box-top, .tpd-background-box-bottom"
        );
        smallBoxes.css({
          height: this._vars.cut,
          width: this._css.border
        });

        // align the bottom side with the bottom
        f.find(".tpd-background-box-bottom")
          .css({
            top: value.dimensions.height - this._vars.cut
          })
          // shift right side back
          .find(".tpd-background-box-shift")
          .css({
            "margin-top": -1 * value.dimensions.height + this._vars.cut
          });

        // both sides should now be moved left or right depending on the current side
        var moveSmallBy =
          name === "right"
            ? value.dimensions.width - value.stemPx - this._css.border
            : 0;
        smallBoxes
          .css({
            left: moveSmallBy
          })
          .find(".tpd-background-box-shift")
          .css({
            // inverse of the above
            "margin-left": -1 * moveSmallBy
          });

        // hide the background that will be replaced by the stemshift when we have a stem
        f.find(
          ".tpd-background-box-" + (name == "left" ? "left" : "right")
        ).hide();

        // resize the other one
        if (name === "right") {
          // top can be resized to height - border
          f.find(".tpd-background-box-left").css({
            width: value.dimensions.width - value.stemPx - this._css.border
          });
        } else {
          f.find(".tpd-background-box-right")
            .css({
              "margin-left": this._css.border //,
              //height: (value.dimensions.height - value.stemPx - this._vars.border) + 'px'
            })
            .find(".tpd-background-box-shift")
            .css({
              "margin-left": -1 * this._css.border
            });
        }

        // left or right should be shifted to the center
        // depending on which side is used
        var smallBox = f.find(".tpd-background-box-" + this._side);
        smallBox.css({
          height: value.dimensions.height - 2 * this._vars.cut, // resize
          "margin-top": this._vars.cut
        });
        smallBox.find(".tpd-background-box-shift").css({
          "margin-top": -1 * this._vars.cut
        });
      } else {
        // top or bottom
        // make left and right side small
        var smallBoxes = f.find(
          ".tpd-background-box-left, .tpd-background-box-right"
        );
        smallBoxes.css({
          width: this._vars.cut,
          height: this._css.border
        });

        // align the right side with the right
        f.find(".tpd-background-box-right")
          .css({
            left: value.dimensions.width - this._vars.cut
          })
          // shift right side back
          .find(".tpd-background-box-shift")
          .css({
            "margin-left": -1 * value.dimensions.width + this._vars.cut
          });

        // both sides should now be moved up or down depending on the current side
        var moveSmallBy =
          name === "bottom"
            ? value.dimensions.height - value.stemPx - this._css.border
            : 0;
        smallBoxes
          .css({
            top: moveSmallBy
          })
          .find(".tpd-background-box-shift")
          .css({
            // inverse of the above
            "margin-top": -1 * moveSmallBy
          });

        // hide the background that will be replaced by the stemshift
        f.find(
          ".tpd-background-box-" + (name === "top" ? "top" : "bottom")
        ).hide();

        // resize the other one
        if (name === "bottom") {
          // top can be resized to height - border
          f.find(".tpd-background-box-top").css({
            height: value.dimensions.height - value.stemPx - this._css.border
          });
        } else {
          f.find(".tpd-background-box-bottom")
            .css({
              "margin-top": this._css.border
            })
            .find(".tpd-background-box-shift")
            .css({
              "margin-top": -1 * this._css.border
            });
        }

        // top or bottom should be shifted to the center
        // depending on which side is used
        var smallBox = f.find(".tpd-background-box-" + this._side);
        smallBox.css({
          width: value.dimensions.width - 2 * this._vars.cut,
          "margin-left": this._vars.cut
        });
        smallBox.find(".tpd-background-box-shift").css({
          "margin-left": -1 * this._vars.cut
        });
      }
    }

    // position the loader
    var fb = frame.background,
      fbp = fb.position,
      fbd = fb.dimensions;
    this._spinner.css({
      top:
        fbp.top +
        this._css.border +
        (fbd.height * 0.5 - this._css.spinner.dimensions.height * 0.5),
      left:
        fbp.left +
        this._css.border +
        (fbd.width * 0.5 - this._css.spinner.dimensions.width * 0.5)
    });
  },

  getVars: function() {
    var padding = this._css.padding,
      radius = this._css.radius,
      border = this._css.border;

    var maxStemHeight = this._vars.maxStemHeight || 0;
    var dimensions = $.extend({}, this._dimensions || {});
    var vars = {
      frames: {},
      dimensions: dimensions,
      maxStemHeight: maxStemHeight
    };

    // set the cut
    vars.cut = Math.max(this._css.border, this._css.radius) || 0;

    var stemDimensions = { width: 0, height: 0 };
    var stemOffset = 0;
    var stemPx = 0;

    if (this.tooltip.options.stem) {
      stemDimensions = this.stem_top.getMath().dimensions.outside;
      stemOffset = this.stem_top._css.offset;
      stemPx = Math.max(stemDimensions.height - this._css.border, 0); // the height we assume the stem is should never be negative
    }

    // store for later use
    vars.stemDimensions = stemDimensions;
    vars.stemOffset = stemOffset;

    // positition the background and resize the outer frame
    $.each(
      "top right bottom left".split(" "),
      $.proxy(function(i, side) {
        var orientation = Position.getOrientation(side),
          isLR = orientation === "vertical";

        var frameDimensions = {
          width: dimensions.width + 2 * border,
          height: dimensions.height + 2 * border
        };

        var shiftWidth =
          frameDimensions[isLR ? "height" : "width"] - 2 * vars.cut;

        var frame = {
          dimensions: frameDimensions,
          stemPx: stemPx,
          position: { top: 0, left: 0 },
          background: {
            dimensions: $.extend({}, dimensions),
            position: { top: 0, left: 0 }
          }
        };
        vars.frames[side] = frame;

        // adjust width or height of frame based on orientation
        frame.dimensions[isLR ? "width" : "height"] += stemPx;

        if (side === "top" || side === "left") {
          frame.background.position[side] += stemPx;
        }

        $.extend(frame, {
          shift: {
            position: { top: 0, left: 0 },
            dimensions: {
              width: isLR ? stemDimensions.height : shiftWidth,
              height: isLR ? shiftWidth : stemDimensions.height
            }
          }
        });

        switch (side) {
          case "top":
          case "bottom":
            frame.shift.position.left += vars.cut;

            if (side === "bottom") {
              frame.shift.position.top +=
                frameDimensions.height - border - stemPx;
            }
            break;
          case "left":
          case "right":
            frame.shift.position.top += vars.cut;

            if (side === "right") {
              frame.shift.position.left +=
                frameDimensions.width - border - stemPx;
            }
            break;
        }
      }, this)
    );

    // add connections
    vars.connections = {};
    $.each(
      Position.positions,
      $.proxy(function(i, position) {
        vars.connections[position] = this.getConnectionLayout(position, vars);
      }, this)
    );

    return vars;
  },

  setDimensions: function(dimensions) {
    this.build();

    // don't update if nothing changed
    var d = this._dimensions;
    if (d && d.width === dimensions.width && d.height === dimensions.height) {
      return;
    }

    this._dimensions = dimensions;
    this._vars = this.getVars();
  },

  setSide: function(side) {
    this._side = side;
    this._vars = this.getVars();
  },

  // gets position and offset of the given stem
  getConnectionLayout: function(position, vars) {
    var side = Position.getSide(position),
      orientation = Position.getOrientation(position),
      dimensions = vars.dimensions,
      cut = vars.cut; // where the stem starts

    var stem = this["stem_" + side],
      stemOffset = vars.stemOffset,
      stemWidth = this.tooltip.options.stem
        ? stem.getMath().dimensions.outside.width
        : 0,
      stemMiddleFromSide = cut + stemOffset + stemWidth * 0.5;

    // at the end of this function we should know how much the stem is able to shift
    var layout = {
      stem: {}
    };
    var move = {
      left: 0,
      right: 0,
      up: 0,
      down: 0
    };

    var stemConnection = { top: 0, left: 0 },
      connection = { top: 0, left: 0 };

    var frame = vars.frames[side],
      stemMiddleFromSide = 0;

    // top/bottom
    if (orientation == "horizontal") {
      var width = frame.dimensions.width;

      if (this.tooltip.options.stem) {
        width = frame.shift.dimensions.width;

        // if there's not enough width for twice the stemOffset, calculate what is available, divide the width
        if (width - stemWidth < 2 * stemOffset) {
          stemOffset = Math.floor((width - stemWidth) * 0.5) || 0;
        }

        stemMiddleFromSide = cut + stemOffset + stemWidth * 0.5;
      }

      var availableWidth = width - 2 * stemOffset;

      var split = Position.split(position);
      var left = stemOffset;
      switch (split[2]) {
        case "left":
          move.right = availableWidth - stemWidth;

          stemConnection.left = stemMiddleFromSide;
          break;
        case "middle":
          left += Math.round(availableWidth * 0.5 - stemWidth * 0.5);

          move.left = left - stemOffset;
          move.right = left - stemOffset;

          stemConnection.left = connection.left = Math.round(
            frame.dimensions.width * 0.5
          );
          //connection.left = stemConnection.left;
          break;
        case "right":
          left += availableWidth - stemWidth;

          move.left = availableWidth - stemWidth;

          stemConnection.left = frame.dimensions.width - stemMiddleFromSide;
          connection.left = frame.dimensions.width;
          break;
      }

      // if we're working with the bottom stems we have to add the height to the connection
      if (split[1] === "bottom") {
        stemConnection.top += frame.dimensions.height;
        connection.top += frame.dimensions.height;
      }

      $.extend(layout.stem, {
        position: { left: left },
        before: { width: left },
        after: {
          left: left + stemWidth,
          //right: 0, // seems to work better in Chrome (subpixel bug)
          // but it fails in oldIE, se we add overlap to compensate
          width: width - left - stemWidth + 1
        }
      });
    } else {
      // we are dealing with height
      var height = frame.dimensions.height;

      if (this.tooltip.options.stem) {
        height = frame.shift.dimensions.height;

        if (height - stemWidth < 2 * stemOffset) {
          stemOffset = Math.floor((height - stemWidth) * 0.5) || 0;
        }

        stemMiddleFromSide = cut + stemOffset + stemWidth * 0.5;
      }

      var availableHeight = height - 2 * stemOffset;

      var split = Position.split(position);
      var top = stemOffset;
      switch (split[2]) {
        case "top":
          move.down = availableHeight - stemWidth;

          stemConnection.top = stemMiddleFromSide;
          break;
        case "middle":
          top += Math.round(availableHeight * 0.5 - stemWidth * 0.5);

          move.up = top - stemOffset;
          move.down = top - stemOffset;

          stemConnection.top = connection.top = Math.round(
            frame.dimensions.height * 0.5
          );
          break;
        case "bottom":
          top += availableHeight - stemWidth;

          move.up = availableHeight - stemWidth;

          stemConnection.top = frame.dimensions.height - stemMiddleFromSide;
          connection.top = frame.dimensions.height;
          break;
      }

      // if we're working with the right stems we have to add the height to the connection
      if (split[1] === "right") {
        stemConnection.left += frame.dimensions.width;
        connection.left += frame.dimensions.width;
      }

      $.extend(layout.stem, {
        position: { top: top },
        before: { height: top },
        after: {
          top: top + stemWidth,
          height: height - top - stemWidth + 1
        }
      });
    }

    // store movement and connection
    layout.move = move;
    layout.stem.connection = stemConnection;
    layout.connection = connection;

    return layout;
  },

  // sets the stem as one of the available 12 positions
  // we also need to call this function without a stem because it sets
  // connections
  setStemPosition: function(stemPosition, shift) {
    if (this._stemPosition !== stemPosition) {
      this._stemPosition = stemPosition;
      var side = Position.getSide(stemPosition);
      this.setSide(side);
    }

    // actual positioning
    if (this.tooltip.options.stem) {
      this.setStemShift(stemPosition, shift);
    }
  },

  setStemShift: function(stemPosition, shift) {
    var _shift = this._shift,
      _dimensions = this._dimensions;
    // return if we have the same shift on the same dimensions
    if (
      _shift &&
      _shift.stemPosition === stemPosition &&
      _shift.shift.x === shift.x &&
      _shift.shift.y === shift.y &&
      _dimensions &&
      _shift.dimensions.width === _dimensions.width &&
      _shift.dimensions.height === _dimensions.height
    ) {
      return;
    }
    this._shift = {
      stemPosition: stemPosition,
      shift: shift,
      dimensions: _dimensions
    };

    var side = Position.getSide(stemPosition),
      xy = { horizontal: "x", vertical: "y" }[
        Position.getOrientation(stemPosition)
      ],
      leftWidth = {
        x: { left: "left", width: "width" },
        y: { left: "top", width: "height" }
      }[xy],
      stem = this["stem_" + side],
      layout = deepExtend({}, this._vars.connections[stemPosition].stem);

    // only use offset in the orientation of this position
    if (shift && shift[xy] !== 0) {
      layout.before[leftWidth["width"]] += shift[xy];
      layout.position[leftWidth["left"]] += shift[xy];
      layout.after[leftWidth["left"]] += shift[xy];
      layout.after[leftWidth["width"]] -= shift[xy];
    }

    // actual positioning
    stem.element.css(layout.position);
    stem.element.siblings(".tpd-shift-stem-side-before").css(layout.before);
    stem.element.siblings(".tpd-shift-stem-side-after").css(layout.after);
  }
});

function Stem() {
  this.initialize.apply(this, _slice.call(arguments));
}

$.extend(Stem.prototype, {
  initialize: function(element, skin) {
    this.element = $(element);
    if (!this.element[0]) return;

    this.skin = skin;

    this.element.removeClass("tpd-stem-reset"); // for correct offset
    this._css = $.extend({}, skin._css, {
      width: this.element.innerWidth(),
      height: this.element.innerHeight(),
      offset: parseFloat(this.element.css("margin-left")), // side
      spacing: parseFloat(this.element.css("margin-top"))
    });
    this.element.addClass("tpd-stem-reset");

    this.options = $.extend({}, arguments[2] || {});

    this._position = this.element.attr("data-stem-position") || "top";
    this._m = 100; // multiplier, improves rendering when scaling everything down

    this.build();
  },

  destroy: function() {
    this.element.html("");
  },

  build: function() {
    this.destroy();

    // figure out low opacity based on the background color
    var backgroundColor = this._css.backgroundColor,
      alpha =
        backgroundColor.indexOf("rgba") > -1 &&
        parseFloat(backgroundColor.replace(/^.*,(.+)\)/, "$1")),
      hasLowOpacityTriangle = alpha && alpha < 1;

    // if the triangle doesn't have opacity or when we don't have to deal with a border
    // we can get away with a better way to draw the stem.
    // otherwise we need to draw the border as a seperate element, but that
    // can only happen on browsers with support for transforms
    this._useTransform = hasLowOpacityTriangle && Support.css.transform;
    if (!this._css.border) this._useTransform = false;
    this[(this._useTransform ? "build" : "buildNo") + "Transform"]();
  },

  buildTransform: function() {
    this.element.append(
      (this.spacer = $("<div>")
        .addClass("tpd-stem-spacer")
        .append(
          (this.downscale = $("<div>")
            .addClass("tpd-stem-downscale")
            .append(
              (this.transform = $("<div>")
                .addClass("tpd-stem-transform")
                .append(
                  (this.first = $("<div>")
                    .addClass("tpd-stem-side")
                    .append(
                      (this.border = $("<div>").addClass("tpd-stem-border"))
                    )
                    .append($("<div>").addClass("tpd-stem-border-corner"))
                    .append($("<div>").addClass("tpd-stem-triangle")))
                ))
            ))
        ))
    );
    this.transform.append(
      (this.last = this.first.clone().addClass("tpd-stem-side-inversed"))
    );
    this.sides = this.first.add(this.last);

    var math = this.getMath(),
      md = math.dimensions,
      _m = this._m,
      _side = Position.getSide(this._position);

    //if (!math.enabled) return;

    this.element.find(".tpd-stem-spacer").css({
      width: _flip ? md.inside.height : md.inside.width,
      height: _flip ? md.inside.width : md.inside.height
    });
    if (_side === "top" || _side === "left") {
      var _scss = {};
      if (_side === "top") {
        _scss.bottom = 0;
        _scss.top = "auto";
      } else if (_side === "left") {
        _scss.right = 0;
        _scss.left = "auto";
      }

      this.element.find(".tpd-stem-spacer").css(_scss);
    }

    this.transform.css({
      width: md.inside.width * _m,
      height: md.inside.height * _m
    });

    // adjust the dimensions of the element to that of the
    var _transform = Support.css.prefixed("transform");

    // triangle
    var triangleStyle = {
      "background-color": "transparent",
      "border-bottom-color": this._css.backgroundColor,
      "border-left-width": md.inside.width * 0.5 * _m,
      "border-bottom-width": md.inside.height * _m
    };
    triangleStyle[_transform] = "translate(" + math.border * _m + "px, 0)";
    this.element.find(".tpd-stem-triangle").css(triangleStyle);

    // border
    // first convert color to rgb + opacity
    // otherwise we'd be working with a border that overlays the background
    var borderColor = this._css.borderColor;
    alpha =
      borderColor.indexOf("rgba") > -1 &&
      parseFloat(borderColor.replace(/^.*,(.+)\)/, "$1"));
    if (alpha && alpha < 1) {
      // turn the borderColor into a color without alpha
      borderColor = (
        borderColor.substring(0, borderColor.lastIndexOf(",")) + ")"
      ).replace("rgba", "rgb");
    } else {
      alpha = 1;
    }

    var borderStyle = {
      "background-color": "transparent",
      "border-right-width": math.border * _m,
      width: math.border * _m,
      "margin-left": -2 * math.border * _m,
      "border-color": borderColor,
      opacity: alpha
    };
    borderStyle[_transform] =
      "skew(" +
      math.skew +
      "deg) translate(" +
      math.border * _m +
      "px, " +
      -1 * this._css.border * _m +
      "px)";
    this.element.find(".tpd-stem-border").css(borderStyle);

    var borderColor = this._css.borderColor;
    alpha =
      borderColor.indexOf("rgba") > -1 &&
      parseFloat(borderColor.replace(/^.*,(.+)\)/, "$1"));
    if (alpha && alpha < 1) {
      // turn the borderColor into a color without alpha
      borderColor = (
        borderColor.substring(0, borderColor.lastIndexOf(",")) + ")"
      ).replace("rgba", "rgb");
    } else {
      alpha = 1;
    }

    var borderCornerStyle = {
      width: math.border * _m,
      "border-right-width": math.border * _m,
      "border-right-color": borderColor,
      background: borderColor,
      opacity: alpha,
      // setting opacity here causes a flicker in firefox, it's set in css now
      // 'opacity': this._css.borderOpacity,
      "margin-left": -2 * math.border * _m
    };
    borderCornerStyle[_transform] =
      "skew(" +
      math.skew +
      "deg) translate(" +
      math.border * _m +
      "px, " +
      (md.inside.height - this._css.border) * _m +
      "px)";

    this.element.find(".tpd-stem-border-corner").css(borderCornerStyle);

    // measurements are done, now flip things if needed
    this.setPosition(this._position);

    // now downscale to improve subpixel rendering
    if (_m > 1) {
      var t = {};
      t[_transform] = "scale(" + 1 / _m + "," + 1 / _m + ")";
      this.downscale.css(t);
    }
    // switch around the visible dimensions if needed
    var _flip = /^(left|right)$/.test(this._position);

    if (!this._css.border) {
      this.element.find(".tpd-stem-border, .tpd-stem-border-corner").hide();
    }

    this.element.css({
      width: _flip ? md.outside.height : md.outside.width,
      height: _flip ? md.outside.width : md.outside.height
    });
  },

  buildNoTransform: function() {
    this.element.append(
      (this.spacer = $("<div>")
        .addClass("tpd-stem-spacer")
        .append(
          $("<div>")
            .addClass("tpd-stem-notransform")
            .append(
              $("<div>")
                .addClass("tpd-stem-border")
                .append($("<div>").addClass("tpd-stem-border-corner"))
                .append(
                  $("<div>")
                    .addClass("tpd-stem-border-center-offset")
                    .append(
                      $("<div>")
                        .addClass("tpd-stem-border-center-offset-inverse")
                        .append($("<div>").addClass("tpd-stem-border-center"))
                    )
                )
            )
            .append($("<div>").addClass("tpd-stem-triangle"))
        ))
    );

    var math = this.getMath(),
      md = math.dimensions;

    var _flip = /^(left|right)$/.test(this._position),
      _bottom = /^(bottom)$/.test(this._position),
      _right = /^(right)$/.test(this._position),
      _side = Position.getSide(this._position);

    this.element.css({
      width: _flip ? md.outside.height : md.outside.width,
      height: _flip ? md.outside.width : md.outside.height
    });

    // handle spacer
    this.element
      .find(".tpd-stem-notransform")
      .add(this.element.find(".tpd-stem-spacer"))
      .css({
        width: _flip ? md.inside.height : md.inside.width,
        height: _flip ? md.inside.width : md.inside.height
      });
    if (_side === "top" || _side === "left") {
      var _scss = {};
      if (_side === "top") {
        _scss.bottom = 0;
        _scss.top = "auto";
      } else if (_side === "left") {
        _scss.right = 0;
        _scss.left = "auto";
      }

      this.element.find(".tpd-stem-spacer").css(_scss);
    }

    // resets
    this.element.find(".tpd-stem-border").css({
      width: "100%",
      background: "transparent"
    });

    // == on bottom
    var borderCornerStyle = {
      opacity: 1
    };

    borderCornerStyle[_flip ? "height" : "width"] = "100%";
    borderCornerStyle[_flip ? "width" : "height"] = this._css.border;
    borderCornerStyle[_bottom ? "top" : "bottom"] = 0;

    $.extend(borderCornerStyle, !_right ? { right: 0 } : { left: 0 });

    this.element.find(".tpd-stem-border-corner").css(borderCornerStyle);

    // border /\
    // top or bottom
    var borderStyle = {
      width: 0,
      "background-color": "transparent",
      opacity: 1
    };

    var borderSideCSS = md.inside.width * 0.5 + "px solid transparent";

    var triangleStyle = { "background-color": "transparent" };
    var triangleSideCSS =
      md.inside.width * 0.5 - math.border + "px solid transparent";

    if (!_flip) {
      var shared = {
        "margin-left": -0.5 * md.inside.width,
        "border-left": borderSideCSS,
        "border-right": borderSideCSS
      };

      // ==
      $.extend(borderStyle, shared);
      borderStyle[_bottom ? "border-top" : "border-bottom"] =
        md.inside.height + "px solid " + this._css.borderColor;

      // /\
      $.extend(triangleStyle, shared);
      triangleStyle[_bottom ? "border-top" : "border-bottom"] =
        md.inside.height + "px solid " + this._css.backgroundColor;
      triangleStyle[!_bottom ? "top" : "bottom"] = math.top;
      triangleStyle[_bottom ? "top" : "bottom"] = "auto";

      // add offset
      this.element
        .find(".tpd-stem-border-center-offset")
        .css({
          "margin-top": -1 * this._css.border * (_bottom ? -1 : 1)
        })
        .find(".tpd-stem-border-center-offset-inverse")
        .css({
          "margin-top": this._css.border * (_bottom ? -1 : 1)
        });
    } else {
      var shared = {
        left: "auto",
        top: "50%",
        "margin-top": -0.5 * md.inside.width,
        "border-top": borderSideCSS,
        "border-bottom": borderSideCSS
      };

      // ==
      $.extend(borderStyle, shared);
      borderStyle[_right ? "right" : "left"] = 0;
      borderStyle[_right ? "border-left" : "border-right"] =
        md.inside.height + "px solid " + this._css.borderColor;

      // /\
      $.extend(triangleStyle, shared);
      triangleStyle[_right ? "border-left" : "border-right"] =
        md.inside.height + "px solid " + this._css.backgroundColor;
      triangleStyle[!_right ? "left" : "right"] = math.top;
      triangleStyle[_right ? "left" : "right"] = "auto";

      // add offset
      this.element
        .find(".tpd-stem-border-center-offset")
        .css({
          "margin-left": -1 * this._css.border * (_right ? -1 : 1)
        })
        .find(".tpd-stem-border-center-offset-inverse")
        .css({
          "margin-left": this._css.border * (_right ? -1 : 1)
        });
    }

    this.element.find(".tpd-stem-border-center").css(borderStyle);
    this.element
      .find(".tpd-stem-border-corner")
      .css({ "background-color": this._css.borderColor });
    this.element.find(".tpd-stem-triangle").css(triangleStyle);

    if (!this._css.border) {
      this.element.find(".tpd-stem-border").hide();
    }
  },

  setPosition: function(position) {
    this._position = position;
    this.transform.attr(
      "class",
      "tpd-stem-transform tpd-stem-transform-" + position
    );
  },

  getMath: function() {
    var height = this._css.height,
      width = this._css.width,
      border = this._css.border;

    // width should be divisible by 2
    // this fixes pixel bugs in the transform methods, so only do it there
    if (this._useTransform && !!(Math.floor(width) % 2)) {
      width = Math.max(Math.floor(width) - 1, 0);
    }

    // first increase the original dimensions so the triangle is that of the given css dimensions
    var corner_top = degrees(Math.atan((width * 0.5) / height)),
      corner_side = 90 - corner_top,
      side = border / Math.cos(((90 - corner_side) * Math.PI) / 180),
      top = border / Math.cos(((90 - corner_top) * Math.PI) / 180);
    var dimensions = {
      width: width + side * 2,
      height: height + top
    };

    var cut = Math.max(border, this._css.radius);

    // adjust height and width
    height = dimensions.height;
    width = dimensions.width * 0.5;

    // calculate the rest
    var cA = degrees(Math.atan(height / width)),
      cB = 90 - cA,
      overstaand = border / Math.cos((cB * Math.PI) / 180);

    var angle = (Math.atan(height / width) * 180) / Math.PI,
      skew = -1 * (90 - angle),
      angleTop = 90 - angle,
      cornerWidth = border * Math.tan((angleTop * Math.PI) / 180);

    var top = border / Math.cos(((90 - angleTop) * Math.PI) / 180);

    // add spacing
    //dimensions.height += this._css.spacing;
    var inside = $.extend({}, dimensions),
      outside = $.extend({}, dimensions);
    outside.height += this._css.spacing;

    // IE11 and below have issues with rendering stems that
    // end up with floating point dimensions
    // ceil the outside height to fix this
    outside.height = Math.ceil(outside.height);

    // if the border * 2 is bigger than the width, we should disable the stem
    var enabled = true;
    if (border * 2 >= dimensions.width) {
      enabled = false;
    }

    return {
      enabled: enabled,
      outside: outside,
      dimensions: {
        inside: inside,
        outside: outside
      },
      top: top,
      border: overstaand,
      skew: skew,
      corner: cornerWidth
    };
  }
});

var Tooltips = {
  tooltips: {},

  options: {
    defaultSkin: "dark",
    startingZIndex: 999999
  },

  _emptyClickHandler: function() {},

  init: function() {
    this.reset();

    this._resizeHandler = $.proxy(this.onWindowResize, this);
    $(window).on("resize orientationchange", this._resizeHandler);

    if (Browser.MobileSafari) {
      $("body").on("click", this._emptyClickHandler);
    }
  },

  reset: function() {
    Tooltips.removeAll();

    Delegations.removeAll();

    if (this._resizeHandler) {
      $(window).off("resize orientationchange", this._resizeHandler);
    }

    if (Browser.MobileSafari) {
      $("body").off("click", this._emptyClickHandler);
    }
  },

  onWindowResize: function() {
    if (this._resizeTimer) {
      window.clearTimeout(this._resizeTimer);
      this._resizeTimer = null;
    }

    this._resizeTimer = _.delay(
      $.proxy(function() {
        var visible = this.getVisible();
        $.each(visible, function(i, tooltip) {
          tooltip.clearUpdatedTo();

          tooltip.position();
        });
      }, this),
      15
    );
  },

  _getTooltips: function(element, noClosest) {
    var uids = [],
      tooltips = [],
      u;

    if (_.isElement(element)) {
      if ((u = $(element).data("tipped-uids"))) uids = uids.concat(u);
    } else {
      // selector
      $(element).each(function(i, el) {
        if ((u = $(el).data("tipped-uids"))) uids = uids.concat(u);
      });
    }

    if (!uids[0] && !noClosest) {
      // find a uids string
      var closestTooltip = this.getTooltipByTooltipElement(
        $(element).closest(".tpd-tooltip")[0]
      );
      if (closestTooltip && closestTooltip.element) {
        u = $(closestTooltip.element).data("tipped-uids") || [];
        if (u) uids = uids.concat(u);
      }
    }

    if (uids.length > 0) {
      $.each(
        uids,
        $.proxy(function(i, uid) {
          var tooltip;
          if ((tooltip = this.tooltips[uid])) {
            tooltips.push(tooltip);
          }
        }, this)
      );
    }

    return tooltips;
  },

  // Returns the element for which the tooltip was created when given a tooltip element or any element within that tooltip.
  findElement: function(element) {
    var tooltips = [];

    if (_.isElement(element)) {
      tooltips = this._getTooltips(element);
    }

    return tooltips[0] && tooltips[0].element;
  },

  get: function(element) {
    var options = $.extend(
      {
        api: false
      },
      arguments[1] || {}
    );

    var matched = [];
    if (_.isElement(element)) {
      matched = this._getTooltips(element);
    } else if (element instanceof $) {
      // when a jQuery object, search every element
      element.each(
        $.proxy(function(i, el) {
          var tooltips = this._getTooltips(el, true);
          if (tooltips.length > 0) {
            matched = matched.concat(tooltips);
          }
        }, this)
      );
    } else if (typeof element === "string") {
      // selector
      $.each(this.tooltips, function(i, tooltip) {
        if (tooltip.element && $(tooltip.element).is(element)) {
          matched.push(tooltip);
        }
      });
    }

    // if api is set we'll mark the given tooltips as using the API
    if (options.api) {
      $.each(matched, function(i, tooltip) {
        tooltip.is("api", true);
      });
    }

    return matched;
  },

  getTooltipByTooltipElement: function(element) {
    if (!element) return null;
    var matched = null;
    $.each(this.tooltips, function(i, tooltip) {
      if (tooltip.is("build") && tooltip._tooltip[0] === element) {
        matched = tooltip;
      }
    });
    return matched;
  },

  getBySelector: function(selector) {
    var matched = [];
    $.each(this.tooltips, function(i, tooltip) {
      if (tooltip.element && $(tooltip.element).is(selector)) {
        matched.push(tooltip);
      }
    });
    return matched;
  },

  getNests: function() {
    var matched = [];
    $.each(this.tooltips, function(i, tooltip) {
      if (tooltip.is("nest")) {
        // safe cause when a tooltip is a nest it's already build
        matched.push(tooltip);
      }
    });
    return matched;
  },

  show: function(selector) {
    $(this.get(selector)).each(function(i, tooltip) {
      tooltip.show(false, true); // not instant, but without delay
    });
  },

  hide: function(selector) {
    $(this.get(selector)).each(function(i, tooltip) {
      tooltip.hide();
    });
  },

  toggle: function(selector) {
    $(this.get(selector)).each(function(i, tooltip) {
      tooltip.toggle();
    });
  },

  hideAll: function(but) {
    $.each(this.getVisible(), function(i, tooltip) {
      if (but && but === tooltip) return;
      tooltip.hide();
    });
  },

  refresh: function(selector) {
    // find only those tooltips that are visible
    var tooltips;
    if (selector) {
      // filter out only those visible
      tooltips = $.grep(this.get(selector), function(tooltip, i) {
        return tooltip.is("visible");
      });
    } else {
      // all visible tooltips
      tooltips = this.getVisible();
    }

    $.each(tooltips, function(i, tooltip) {
      tooltip.refresh();
    });
  },

  getVisible: function() {
    var visible = [];
    $.each(this.tooltips, function(i, tooltip) {
      if (tooltip.visible()) {
        visible.push(tooltip);
      }
    });
    return visible;
  },

  isVisibleByElement: function(element) {
    var visible = false;
    if (_.isElement(element)) {
      $.each(this.getVisible() || [], function(i, tooltip) {
        if (tooltip.element === element) {
          visible = true;
          return false;
        }
      });
    }
    return visible;
  },

  getHighestTooltip: function() {
    var Z = 0,
      h;
    $.each(this.tooltips, function(i, tooltip) {
      if (tooltip.zIndex > Z) {
        Z = tooltip.zIndex;
        h = tooltip;
      }
    });
    return h;
  },

  resetZ: function() {
    // the zIndex only has to be restore when there are no visible tooltip
    // use find to $break when a a visible tooltip is found
    if (this.getVisible().length <= 1) {
      $.each(this.tooltips, function(i, tooltip) {
        // only reset on tooltip that don't have the zIndex option set
        if (tooltip.is("build") && !tooltip.options.zIndex) {
          tooltip._tooltip.css({
            zIndex: (tooltip.zIndex = +Tooltips.options.startingZIndex)
          });
        }
      });
    }
  },

  // AjaxCache
  clearAjaxCache: function() {
    // if there's an _cache.xhr running, abort it for all tooltips
    // set updated state to false for all
    $.each(
      this.tooltips,
      $.proxy(function(i, tooltip) {
        if (tooltip.options.ajax) {
          // abort possible running request
          if (tooltip._cache && tooltip._cache.xhr) {
            tooltip._cache.xhr.abort();
            tooltip._cache.xhr = null;
          }

          // reset state
          tooltip.is("updated", false);
          tooltip.is("updating", false);
          tooltip.is("sanitized", false); // sanitize again
        }
      }, this)
    );

    AjaxCache.clear();
  },

  add: function(tooltip) {
    this.tooltips[tooltip.uid] = tooltip;
  },

  remove: function(element) {
    var tooltips = this._getTooltips(element);
    this.removeTooltips(tooltips);
  },

  removeTooltips: function(tooltips) {
    if (!tooltips) return;

    $.each(
      tooltips,
      $.proxy(function(i, tooltip) {
        var uid = tooltip.uid;

        delete this.tooltips[uid];

        tooltip.remove(); // also removes uid from element
      }, this)
    );
  },

  // remove all tooltips that are not attached to the DOM
  removeDetached: function() {
    // first find all nests
    var nests = this.getNests(),
      detached = [];
    if (nests.length > 0) {
      $.each(nests, function(i, nest) {
        if (nest.is("detached")) {
          detached.push(nest);
          nest.attach();
        }
      });
    }

    $.each(
      this.tooltips,
      $.proxy(function(i, tooltip) {
        if (tooltip.element && !_.element.isAttached(tooltip.element)) {
          this.remove(tooltip.element);
        }
      }, this)
    );

    // restore previously detached nests
    // if they haven't been removed
    $.each(detached, function(i, nest) {
      nest.detach();
    });
  },

  removeAll: function() {
    $.each(
      this.tooltips,
      $.proxy(function(i, tooltip) {
        if (tooltip.element) {
          this.remove(tooltip.element);
        }
      }, this)
    );
    this.tooltips = {};
  },

  setDefaultSkin: function(name) {
    this.options.defaultSkin = name || "dark";
  },

  setStartingZIndex: function(index) {
    this.options.startingZIndex = index || 0;
  }
};

// Extra position functions, used in Options
Tooltips.Position = {
  inversedPosition: {
    left: "right",
    right: "left",
    top: "bottom",
    bottom: "top",
    middle: "middle",
    center: "center"
  },

  getInversedPosition: function(position) {
    var positions = Position.split(position),
      left = positions[1],
      right = positions[2],
      orientation = Position.getOrientation(position),
      options = $.extend(
        {
          horizontal: true,
          vertical: true
        },
        arguments[1] || {}
      );

    if (orientation === "horizontal") {
      if (options.vertical) {
        left = this.inversedPosition[left];
      }
      if (options.horizontal) {
        right = this.inversedPosition[right];
      }
    } else {
      // vertical
      if (options.vertical) {
        right = this.inversedPosition[right];
      }
      if (options.horizontal) {
        left = this.inversedPosition[left];
      }
    }

    return left + right;
  },

  // what we do here is inverse topleft -> bottomleft instead of bottomright
  // and lefttop -> righttop instead of rightbottom
  getTooltipPositionFromTarget: function(position) {
    var positions = Position.split(position);
    return this.getInversedPosition(
      positions[1] + this.inversedPosition[positions[2]]
    );
  }
};

function Tooltip() {
  this.initialize.apply(this, _slice.call(arguments));
}

$.extend(Tooltip.prototype, {
  supportsLoading: Support.css.transform && Support.css.animation,

  initialize: function(element, content) {
    this.element = element;
    if (!this.element) return;

    var options;
    if (
      typeof content === "object" &&
      !(
        _.isElement(content) ||
        _.isText(content) ||
        _.isDocumentFragment(content) ||
        content instanceof $
      )
    ) {
      options = content;
      content = null;
    } else {
      options = arguments[2] || {};
    }

    // append element options if data-tpd-options
    var dataOptions = $(element).data("tipped-options");
    if (dataOptions) {
      options = deepExtend(
        $.extend({}, options),
        eval("({" + dataOptions + "})")
      );
    }

    this.options = Options.create(options);

    // all the garbage goes in here
    this._cache = {
      dimensions: {
        width: 0,
        height: 0
      },
      events: [],
      timers: {},
      layouts: {},
      is: {},
      fnCallFn: "",
      updatedTo: {}
    };

    // queues for effects
    this.queues = {
      showhide: $({})
    };

    // title
    var title =
      $(element).attr("title") || $(element).data("tipped-restore-title");

    if (!content) {
      // grab the content off the attribute
      var dt = $(element).attr("data-tipped");

      if (dt) {
        content = dt;
      } else if (title) {
        content = title;
      }

      if (content) {
        // avoid scripts in title/data-tipped
        var SCRIPT_REGEX = /<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi;
        content = content.replace(SCRIPT_REGEX, "");
      }
    }

    if (
      (!content || (content instanceof $ && !content[0])) &&
      !((this.options.ajax && this.options.ajax.url) || this.options.inline)
    ) {
      this._aborted = true;
      return;
    }

    // backup title
    if (title) {
      // backup the title so we can restore it once the tooltip is removed
      $(element).data("tipped-restore-title", title);
      $(element)[0].setAttribute("title", ""); // IE needs setAttribute
    }

    this.content = content;
    this.title = $(this.element).data("tipped-title");
    if (typeof this.options.title !== "undefined")
      this.title = this.options.title;

    this.zIndex = this.options.zIndex || +Tooltips.options.startingZIndex;

    // make sure the element has a uids array
    var uids = $(element).data("tipped-uids"); //, initial_uid = uid;
    if (!uids) {
      uids = [];
    }

    // generate a new uid
    var uid = getUID();
    this.uid = uid;
    uids.push(uid);

    // store grown uids array back into data
    $(element).data("tipped-uids", uids);

    // mark parent tooltips as being a nest if this tooltip is created on an element within another tooltip
    var parentTooltipElement = $(this.element).closest(".tpd-tooltip")[0],
      parentTooltip;
    if (
      parentTooltipElement &&
      (parentTooltip = Tooltips.getTooltipByTooltipElement(
        parentTooltipElement
      ))
    ) {
      parentTooltip.is("nest", true);
    }

    // set the target
    var target = this.options.target;
    this.target =
      target === "mouse"
        ? this.element
        : target === "element" || !target
        ? this.element
        : _.isElement(target)
        ? target
        : target instanceof $ && target[0]
        ? target[0]
        : this.element;

    // for inline content
    if (this.options.inline) {
      this.content = $("#" + this.options.inline)[0];
    }

    // ajax might not be using ajax: { url: ... } but instead have the 2nd parameter as its url
    // we store _content
    if (this.options.ajax) {
      this.__content = this.content;
    }

    // function as content
    if ( typeof this.content === "function") {
      this._fn = this.content;
    }

    this.preBuild();

    Tooltips.add(this);
  },

  remove: function() {
    this.unbind();

    this.clearTimers();

    // restore content if it was an element attached to the DOM before insertion
    this.restoreElementToMarker();

    this.stopLoading();
    this.abort();

    // delete the tooltip
    if (this.is("build") && this._tooltip) {
      this._tooltip.remove();
      this._tooltip = null;
    }

    var uids = $(this.element).data("tipped-uids") || [];
    var uid_index = $.inArray(this.uid, uids);
    if (uid_index > -1) {
      uids.splice(uid_index, 1);
      $(this.element).data("tipped-uids", uids);
    }

    if (uids.length < 1) {
      // restore title
      var da = "tipped-restore-title",
        r_title;
      if ((r_title = $(this.element).data(da))) {
        // only restore it when the title hasn't been altered
        if (!$(this.element)[0].getAttribute("title") != "") {
          $(this.element).attr("title", r_title);
        }
        // remove the data
        $(this.element).removeData(da);
      }

      // remove the data attribute uid
      $(this.element).removeData("tipped-uids");
    }

    // remove any delegation classes
    var classList = $(this.element).attr("class") || "",
      newClassList = classList
        .replace(/(tpd-delegation-uid-)\d+/g, "")
        .replace(/^\s\s*/, "")
        .replace(/\s\s*$/, ""); // trim whitespace
    $(this.element).attr("class", newClassList);
  },

  detach: function() {
    if (this.options.detach && !this.is("detached")) {
      if (this._tooltip) this._tooltip.detach();
      this.is("detached", true);
    }
  },

  attach: function() {
    if (this.is("detached")) {
      var container;
      if ( typeof this.options.container === "string") {
        var target = this.target;
        if (target === "mouse") {
          target = this.element;
        }

        container = $(
          $(target)
            .closest(this.options.container)
            .first()
        );
      } else {
        container = $(this.options.container);
      }

      // we default to document body, if nothing was found
      if (!container[0]) container = $(document.body);

      container.append(this._tooltip);
      this.is("detached", false);
    }
  },

  preBuild: function() {
    this.is("detached", true);

    var initialCSS = {
      left: "-10000px", // TODO: remove
      top: "-10000px",
      opacity: 0,
      zIndex: this.zIndex
    };

    this._tooltip = $("<div>")
      .addClass("tpd-tooltip")
      .addClass("tpd-skin-" + this.options.skin)
      .addClass("tpd-size-" + this.options.size)
      .css(initialCSS)
      .hide();

    this.createPreBuildObservers();
  },

  build: function() {
    if (this.is("build")) return;

    this.attach();

    this._tooltip.append((this._skin = $("<div>").addClass("tpd-skin"))).append(
      (this._contentWrapper = $("<div>")
        .addClass("tpd-content-wrapper")
        .append(
          (this._contentSpacer = $("<div>")
            .addClass("tpd-content-spacer")
            .append(
              (this._titleWrapper = $("<div>")
                .addClass("tpd-title-wrapper")
                .append(
                  (this._titleSpacer = $("<div>")
                    .addClass("tpd-title-spacer")
                    .append(
                      (this._titleRelative = $("<div>")
                        .addClass("tpd-title-relative")
                        .append(
                          (this._titleRelativePadder = $("<div>")
                            .addClass("tpd-title-relative-padder")
                            .append(
                              (this._title = $("<div>").addClass("tpd-title"))
                            ))
                        ))
                    ))
                )
                .append(
                  (this._close = $("<div>")
                    .addClass("tpd-close")
                    .append(
                      $("<div>")
                        .addClass("tpd-close-icon")
                        .html("&times;")
                    ))
                ))
            )
            .append(
              (this._contentRelative = $("<div>")
                .addClass("tpd-content-relative")
                .append(
                  (this._contentRelativePadder = $("<div>")
                    .addClass("tpd-content-relative-padder")
                    .append(
                      (this._content = $("<div>").addClass("tpd-content"))
                    ))
                )
                .append(
                  (this._inner_close = $("<div>")
                    .addClass("tpd-close")
                    .append(
                      $("<div>")
                        .addClass("tpd-close-icon")
                        .html("&times;")
                    ))
                ))
            ))
        ))
    );

    this.skin = new Skin(this); // TODO: remove instances of is('skinned'), and look into why they are there

    // set radius of contenspacer to be that found on the skin
    this._contentSpacer.css({
      "border-radius": Math.max(
        this.skin._css.radius - this.skin._css.border,
        0
      )
    });

    this.createPostBuildObservers();

    this.is("build", true);
  },

  createPostBuildObservers: function() {
    // x
    this._tooltip.on(
      "click",
      ".tpd-close, .close-tooltip",
      $.proxy(function(event) {
        // this helps prevent the click on x to trigger a click on the body
        // which could conflict with some scripts
        event.stopPropagation();
        event.preventDefault();

        this.is("api", false);

        this.hide(true);
      }, this)
    );
  },

  createPreBuildObservers: function() {
    // what can be observed before build
    // - the element
    this.bind(this.element, "mouseenter", this.setActive); // mousemove
    this.bind(
      this._tooltip,
      // avoid double click issues
      Support.touch && Browser.MobileSafari ? "touchstart" : "mouseenter",
      this.setActive
    );

    // idle stats
    this.bind(this.element, "mouseleave", function(event) {
      this.setIdle(event);
    });
    this.bind(this._tooltip, "mouseleave", function(event) {
      this.setIdle(event);
    });

    if (this.options.showOn) {
      $.each(
        this.options.showOn,
        $.proxy(function(name, events) {
          var element,
            toggleable = false;

          switch (name) {
            case "element":
              element = this.element;

              if (
                this.options.hideOn &&
                this.options.showOn &&
                this.options.hideOn.element === "click" &&
                this.options.showOn.element === "click"
              ) {
                toggleable = true;
                this.is("toggleable", toggleable);
              }
              break;

            case "tooltip":
              element = this._tooltip;
              break;
            case "target":
              element = this.target;
              break;
          }

          if (!element) return;

          if (events) {
            // Translate mouseenter to touchstart
            // just for the tooltip to fix double click issues
            // https://davidwalsh.name/ios-hover-menu-fix
            var useEvents = events;

            this.bind(
              element,
              useEvents,
              events === "click" && toggleable
                ? function(event) {
                    this.is("api", false);
                    this.toggle();
                  }
                : function(event) {
                    this.is("api", false);
                    this.showDelayed();
                  }
            );
          }
        }, this)
      );

      // iOS requires that we track touchend time to avoid
      // links requiring a double-click
      if (Support.touch && Browser.MobileSafari) {
        this.bind(this._tooltip, "touchend", function() {
          this._tooltipTouchEndTime = new Date().getTime();
        });
      }
    }

    if (this.options.hideOn) {
      $.each(
        this.options.hideOn,
        $.proxy(function(name, events) {
          var element;

          switch (name) {
            case "element":
              // no events needed if the element toggles
              if (this.is("toggleable") && events === "click") return;
              element = this.element;
              break;
            case "tooltip":
              element = this._tooltip;
              break;
            case "target":
              element = this.target;
              break;
          }

          // if we don't have an element now we don't have to attach anything
          if (!element) return;

          if (events) {
            var useEvents = events;

            // prevent having to double-click links on iOS
            // by comparing the touchend time on the tooltip to a mouseleave/out
            // triggered on the element or target, if it is within a short duration
            // we cancel the hide event.
            // we basically track if we've moved from element/target to tooltip
            if (
              Support.touch &&
              Browser.MobileSafari &&
              /^(target|element)/.test(name) &&
              /mouse(leave|out)/.test(useEvents)
            ) {
              this.bind(element, useEvents, function(event) {
                if (
                  this._tooltipTouchEndTime &&
                  /^mouse(leave|out)$/.test(event.type)
                ) {
                  var now = new Date().getTime();
                  if (now - this._tooltipTouchEndTime < 450) {
                    // quicktap (355-369ms)
                    return;
                  }
                }
                this.is("api", false);
                this.hideDelayed();
              });
            } else {
              this.bind(element, useEvents, function(event) {
                this.is("api", false);
                this.hideDelayed();
              });
            }
          }
        }, this)
      );
    }

    if (this.options.hideOnClickOutside) {
      // add a class to check for the hideOnClickOutSide element
      $(this.element).addClass("tpd-hideOnClickOutside");

      // touchend is an iOS fix to prevent the need to double tap
      // without this it doesn't even work at all on iOS
      this.bind(
        document.documentElement,
        "click touchend",
        $.proxy(function(event) {
          if (!this.visible()) return;

          var element = $(event.target).closest(
            ".tpd-tooltip, .tpd-hideOnClickOutside"
          )[0];

          if (
            !element ||
            (element &&
              (element !== this._tooltip[0] && element !== this.element))
          ) {
            this.hide();
          }
        }, this)
      );
    }

    if (this.options.target === "mouse") {
      this.bind(
        this.element,
        "mouseenter mousemove",
        $.proxy(function(event) {
          this._cache.event = event;
        }, this)
      );
    }

    var isMouseMove = false;
    if (
      this.options.showOn &&
      this.options.target === "mouse" &&
      !this.options.fixed
    ) {
      isMouseMove = true;
    }

    if (isMouseMove) {
      this.bind(this.element, "mousemove", function(event) {
        if (!this.is("build")) return;
        this.is("api", false);
        this.position();
      });
    }
  }
});

$.extend(Tooltip.prototype, {
  // make sure there are no animations queued up, and stop any animations currently going on
  stop: function() {
    // cancel when we call this function before the tooltip is created
    if (!this._tooltip) return;

    var shq = this.queues.showhide;
    // clear queue
    shq.queue([]);
    // stop possible show/hide event
    this._tooltip.stop(1, 0);
  },

  showDelayed: function(event) {
    if (this.is("disabled")) return;

    // cancel hide timer
    this.clearTimer("hide");

    // if there is a show timer we don't have to start another one
    if (this.is("visible") || this.getTimer("show")) return;

    // otherwise we start one
    this.setTimer(
      "show",
      $.proxy(function() {
        this.clearTimer("show");
        this.show();
      }, this),
      this.options.showDelay || 1
    );
  },

  show: function() {
    this.clearTimer("hide");

    // don't show tooltip already visible or on hidden targets, those would end up at (0, 0)
    if (
      this.visible() ||
      this.is("disabled") ||
      !$(this.target).is(":visible")
    ) {
      return;
    }

    this.is("visible", true);

    this.attach();

    this.stop();
    var shq = this.queues.showhide;

    // update
    if (!(this.is("updated") || this.is("updating"))) {
      shq.queue(
        $.proxy(function(next_updated) {
          this._onResizeDimensions = { width: 0, height: 0 };

          this.update(
            $.proxy(function(aborted) {
              if (aborted) {
                this.is("visible", false);
                this.detach();
                return;
              }

              next_updated();
            }, this)
          );
        }, this)
      );
    }

    // sanitize every time
    // we've moved this outside of the update in 4.3
    // allowing the update to finish without conflicting with the sanitize
    // that might even be performed later or cancelled
    shq.queue(
      $.proxy(function(next_ready_to_show) {
        if (!this.is("sanitized")) {
          this._contentWrapper.css({ visibility: "hidden" });

          this.startLoading();

          this.sanitize(
            $.proxy(function() {
              this.stopLoading();
              this._contentWrapper.css({ visibility: "visible" });
              this.is("resize-to-content", true);
              next_ready_to_show();
            }, this)
          );
        } else {
          // already sanitized
          this.stopLoading(); // always stop loading
          this._contentWrapper.css({ visibility: "visible" }); // and make visible
          this.is("resize-to-content", true);
          next_ready_to_show();
        }
      }, this)
    );

    // position and raise
    // we always do this because when the tooltip hides and ajax updates, we'd otherwise have incorrect dimensions
    shq.queue(
      $.proxy(function(next_position_raise) {
        this.position();
        this.raise();
        next_position_raise();
      }, this)
    );

    // onShow callback
    shq.queue(
      $.proxy(function(next_onshow) {
        // only fire it here if we've already updated
        if (this.is("updated") && typeof this.options.onShow === "function") {
          //
          var visible = new Visible(this._tooltip);
          this.options.onShow(this._content[0], this.element); // todo: update
          visible.restore();
          next_onshow();
        } else {
          next_onshow();
        }
      }, this)
    );

    // Fade-in
    shq.queue(
      $.proxy(function(next_show) {
        this._show(/*instant ? 0 :*/ this.options.fadeIn, function() {
          next_show();
        });
      }, this)
    );
  },

  _show: function(duration, callback) {
    duration =
      ( typeof duration === "number" ? duration : this.options.fadeIn) || 0;
    callback =
      callback || (typeof arguments[0] === "function" ? arguments[0] : false);

    // hide others
    if (this.options.hideOthers) {
      Tooltips.hideAll(this);
    }

    this._tooltip.fadeTo(
      duration,
      1,
      $.proxy(function() {
        if (callback) callback();
      }, this)
    );
  },

  hideDelayed: function() {
    // cancel show timer
    this.clearTimer("show");

    // if there is a hide timer we don't have to start another one
    if (this.getTimer("hide") || !this.visible() || this.is("disabled")) return;

    // otherwise we start one
    this.setTimer(
      "hide",
      $.proxy(function() {
        this.clearTimer("hide");
        this.hide();
      }, this),
      this.options.hideDelay || 1 // always at least some delay
    );
  },

  hide: function(instant, callback) {
    this.clearTimer("show");
    if (!this.visible() || this.is("disabled")) return;

    this.is("visible", false);

    this.stop();
    var shq = this.queues.showhide;

    // instantly cancel ajax/sanitize/refresh
    shq.queue(
      $.proxy(function(next_aborted) {
        this.abort();
        next_aborted();
      }, this)
    );

    // Fade-out
    shq.queue(
      $.proxy(function(next_fade_out) {
        this._hide(instant, next_fade_out);
      }, this)
    );

    // if all tooltips are hidden now we can reset Tooltips.zIndex.current
    shq.queue(function(next_resetZ) {
      Tooltips.resetZ();
      next_resetZ();
    });

    // update on next open
    shq.queue(
      $.proxy(function(next_update_on_show) {
        this.clearUpdatedTo();
        next_update_on_show();
      }, this)
    );

    if (typeof this.options.afterHide === "function" && this.is("updated")) {
      shq.queue(
        $.proxy(function(next_afterhide) {
          this.options.afterHide(this._content[0], this.element); // TODO: update
          next_afterhide();
        }, this)
      );
    }

    // if we have a non-caching ajax or function based tooltip, reset updated
    // after afterHide callback since it checks for this
    if (!this.options.cache && (this.options.ajax || this._fn)) {
      shq.queue(
        $.proxy(function(next_non_cached_reset) {
          this.is("updated", false);
          this.is("updating", false);
          this.is("sanitized", false); // sanitize again
          next_non_cached_reset();
        }, this)
      );
    }

    // callback
    if (typeof callback === "function") {
      shq.queue(function(next_callback) {
        callback();
        next_callback();
      });
    }

    // detach last
    shq.queue(
      $.proxy(function(next_detach) {
        this.detach();
        next_detach();
      }, this)
    );
  },

  _hide: function(instant, callback) {
    callback =
      callback || (typeof arguments[0] === "function" ? arguments[0] : false);

    this.attach();

    // we use fadeTo instead of fadeOut because it has some bugs with detached/reattached elements (jQuery)
    this._tooltip.fadeTo(
      instant ? 0 : this.options.fadeOut,
      0,
      $.proxy(function() {
        // stop loading after a complete hide to make sure a loading icon
        // fades out without switching to content during a hide()
        this.stopLoading();

        // the next show should resize to spinner
        // if it has to sanitize again
        // the logic behind that is handled in show()
        this.is("resize-to-content", false);

        // jQuerys fadein/out is bugged when working with elements that get detached elements
        // fading to 0 doesn't mean we hide at the end, so force that
        this._tooltip.hide();

        if (callback) callback();
      }, this)
    );
  },

  toggle: function() {
    if (this.is("disabled")) return;
    this[this.visible() ? "hide" : "show"]();
  },

  raise: function() {
    // if zIndex is set on the tooltip we don't raise it.
    if (!this.is("build") || this.options.zIndex) return;
    var highestTooltip = Tooltips.getHighestTooltip();

    if (
      highestTooltip &&
      highestTooltip !== this &&
      this.zIndex <= highestTooltip.zIndex
    ) {
      this.zIndex = highestTooltip.zIndex + 1;
      this._tooltip.css({ "z-index": this.zIndex });

      if (this._tooltipShadow) {
        this._tooltipShadow.css({ "z-index": this.zIndex });

        this.zIndex = highestTooltip.zIndex + 2;
        this._tooltip.css({ "z-index": this.zIndex });
      }
    }
  }
});

$.extend(Tooltip.prototype, {
  createElementMarker: function(callback) {
    // marker for inline content
    if (
      !this.elementMarker &&
      this.content &&
      _.element.isAttached(this.content)
    ) {
      // save the original display on the element
      $(this.content).data(
        "tpd-restore-inline-display",
        $(this.content).css("display")
      );

      // put an inline marker before the element
      this.elementMarker = $("<div>").hide();

      $(this.content).before($(this.elementMarker).hide());
    }
  },

  restoreElementToMarker: function() {
    var rid;

    if (this.elementMarker && this.content) {
      // restore old visibility
      if ((rid = $(this.content).data("tpd-restore-inline-display"))) {
        $(this.content).css({ display: rid });
      }

      $(this.elementMarker)
        .before(this.content)
        .remove();
    }
  },

  startLoading: function() {
    if (this.is("loading")) return;

    // make sure the tooltip is build, otherwise there won't be a skin
    this.build();

    // always set this flag
    this.is("loading", true);

    // can exit now if no spinner through options
    if (!this.options.spinner) return;

    this._tooltip.addClass("tpd-is-loading");

    this.skin.startLoading();

    // if we're showing for the first time, force show
    if (!this.is("resize-to-content")) {
      this.position();
      this.raise();
      this._show();
    }
  },

  stopLoading: function() {
    // make sure the tooltip is build, otherwise there won't be a skin
    this.build();

    this.is("loading", false);

    if (!this.options.spinner) return;

    this._tooltip.removeClass("tpd-is-loading");

    this.skin.stopLoading();
  },

  // abort
  abort: function() {
    this.abortAjax();
    this.abortSanitize();
    this.is("refreshed-before-sanitized", false);
  },

  abortSanitize: function() {
    if (this._cache.voila) {
      this._cache.voila.abort();
      this._cache.voila = null;
    }
  },

  abortAjax: function() {
    if (this._cache.xhr) {
      this._cache.xhr.abort();
      this._cache.xhr = null;
      this.is("updated", false);
      this.is("updating", false);
    }
  },

  update: function(callback) {
    if (this.is("updating")) return;

    // mark as updating
    this.is("updating", true);

    this.build();

    var type = this.options.inline
      ? "inline"
      : this.options.ajax
      ? "ajax"
      : _.isElement(this.content) ||
        _.isText(this.content) ||
        _.isDocumentFragment(this.content)
      ? "element"
      : this._fn
      ? "function"
      : "html";

    // it could be that when we update the element that it gets so much content that it overlaps the current mouse position
    // for just a few ms, enough to trigger a mouseleave event. To work around this we hide the tooltip if it was visible.
    // hide the content container while updating, using visibility instead of display to work around
    // issues with scripts that depend on display
    this._contentWrapper.css({ visibility: "hidden" });

    // from here we go into routes that should always return a prepared element to be inserted
    switch (type) {
      case "html":
      case "element":
      case "inline":
        // if we've already updated, just forward to the callback
        if (this.is("updated")) {
          if (callback) callback();
          return;
        }

        this._update(this.content, callback);
        break;

      case "function":
        if (this.is("updated")) {
          if (callback) callback();
          return;
        }

        var updateWith = this._fn(this.element);

        // if there's nothing to update with, abort
        if (!updateWith) {
          this.is("updating", false);
          if (callback) callback(true); // true means aborted in this case
          return;
        }

        this._update(updateWith, callback);
        break;

      case "ajax":
        var ajaxOptions = this.options.ajax || {};

        var url = ajaxOptions.url || this.__content,
          data = ajaxOptions.data || {},
          type = ajaxOptions.type || "GET", // jQuery default
          dataType = ajaxOptions.dataType;

        var initialOptions = { url: url, data: data };
        if (type) $.extend(initialOptions, { type: type }); // keep jQuery initial type intact
        if (dataType) $.extend(initialOptions, { dataType: dataType }); // keep intelligent guess intact

        // merge initial options with given
        var options = $.extend({}, initialOptions, ajaxOptions);

        // remove method from the request, we want to use type only to support jQuery 1.9-
        if (options.method) {
          options = $.extend({}, options);
          delete options.method;
        }

        // make sure there are callbacks
        $.each(
          "complete error success".split(" "),
          $.proxy(function(i, cb) {
            if (!options[cb]) {
              if (cb === "success") {
                // when no success callback is given create a callback that sets
                // the responseText as content, otherwise we use the given one
                options[cb] = function(data, textStatus, jqXHR) {
                  return jqXHR.responseText;
                };
              } else {
                // for every other callback use an empty one
                options[cb] = function() {};
              }
            }

            options[cb] = _.wrap(
              options[cb],
              $.proxy(function(proceed) {
                var args = _slice.call(arguments, 1),
                  jqXHR = (typeof args[0] === "object") ? args[0] : args[2]; // success callback has jqXHR as 3th arg, complete and error as 1st

                // don't store aborts
                if (jqXHR.statusText && jqXHR.statusText === "abort") return;

                // we should cache each individual callback here and make that fetchable
                if (this.options.cache) {
                  AjaxCache.set(
                    {
                      url: options.url,
                      type: options.type,
                      data: options.data
                    },
                    cb,
                    args
                  );
                }

                this._cache.xhr = null;

                // proceed is the callback at this point (complete/success/error)
                // we expect it's return value to hold the value to update the tooltip with
                var updateWith = proceed.apply(this, args);
                if (updateWith) {
                  this._update(updateWith, callback);
                }
              }, this)
            );
          }, this)
        );

        // try cache first, for entries that have previously been successful
        var entry;
        if (
          this.options.cache &&
          (entry = AjaxCache.get(options)) &&
          entry.callbacks.success
        ) {
          // if there is a cache, still call success and complete, but clear out the api
          $.each(
            entry.callbacks,
            $.proxy(function(cb, args) {
              if (typeof options[cb] === "function") {
                options[cb].apply(this, args);
              }
            }, this)
          );

          // stop here and avoid the request
          return;
        }

        // first check cache for possible update object and avoid load if we have one
        this.startLoading();

        this._cache.xhr = $.ajax(options);

        break;
    }
  },

  _update: function(content, callback) {
    // defaults
    var data = {
      title: this.options.title,
      close: this.options.close
    };

    if (
      typeof content === "string" ||
      _.isElement(content) ||
      _.isText(content) ||
      _.isDocumentFragment(content) ||
      content instanceof $
    ) {
      data.content = content;
    } else {
      $.extend(data, content);
    }

    var content = data.content,
      title = data.title,
      close = data.close;

    // store the new content, title and close so dimension/positioning functions can work with it
    this.content = content;
    this.title = title;
    this.close = close;

    // create a marker for when the content is an element attached to the DOM
    this.createElementMarker();

    // make sure the content is visible
    if (_.isElement(content) || content instanceof $) {
      $(content).show();
    }

    // append instantly
    this._content.html(this.content);

    this._title.html(title && typeof title === "string" ? title : "");
    this._titleWrapper[title ? "show" : "hide"]();
    this._close[
      (this.title || this.options.title) && close ? "show" : "hide"
    ]();

    var hasInnerClose = close && !(this.options.title || this.title),
      hasInnerCloseNonOverlap =
        close && !(this.options.title || this.title) && close !== "overlap",
      hasTitleCloseNonOverlap =
        close && (this.options.title || this.title) && close !== "overlap";
    this._inner_close[hasInnerClose ? "show" : "hide"]();
    this._tooltip[(hasInnerCloseNonOverlap ? "add" : "remove") + "Class"](
      "tpd-has-inner-close"
    );
    this._tooltip[(hasTitleCloseNonOverlap ? "add" : "remove") + "Class"](
      "tpd-has-title-close"
    );

    // possible remove padding
    this._content[(this.options.padding ? "remove" : "add") + "Class"](
      "tpd-content-no-padding"
    );

    this.finishUpdate(callback);
  },

  sanitize: function(callback) {
    // if the images loaded plugin isn't loaded, just callback
    if (
      !this.options.voila || // also callback on manual disable
      this._content.find("img").length < 1 // or when no images need preloading
    ) {
      this.is("sanitized", true);
      if (callback) callback();
      return;
    }

    // Voila uses img.complete and polling to detect if an image loaded
    // but if the src of an image is changed, complete will still be true
    // even as it's loading a new source. so we have to fallback to onload
    // to allow for src updates.
    this._cache.voila = Voila(
      this._content,
      { method: "onload" },
      $.proxy(function(instance) {
        // mark images as sanitized so we can avoid sanitizing them again
        // for an instant refresh() later
        this._markImagesAsSanitized(instance.images);

        if (this.is("refreshed-before-sanitized")) {
          this.is("refreshed-before-sanitized", false);
          this.sanitize(callback);
        } else {
          // finish up
          this.is("sanitized", true);
          if (callback) callback();
        }
      }, this)
    );
  },

  // expects a voila.image instance
  _markImagesAsSanitized: function(images) {
    $.each(images, function(i, image) {
      var img = image.img;
      $(img).data("completed-src", image.img.src);
    });
  },

  _hasAllImagesSanitized: function() {
    var sanitizedAll = true;

    // as soon as we find one image that isn't sanitized
    // or sanitized based on the wrong source we
    // have to sanitize again
    this._content.find("img").each(function(i, img) {
      var completedSrc = $(img).data("completed-src");
      if (!(completedSrc && img.src === completedSrc)) {
        sanitizedAll = false;
        return false;
      }
    });

    return sanitizedAll;
  },

  refresh: function() {
    if (!this.visible()) return;

    // avoid refreshing while sanitize() still needs to finish up
    if (!this.is("sanitized")) {
      // mark the need to re-sanitize
      this.is("refreshed-before-sanitized", true);

      return;
    }

    // mark as refreshing
    this.is("refreshing", true);

    // clear potential timers
    this.clearTimer("refresh-spinner");

    if (
      !this.options.voila ||
      this._content.find("img").length < 1 ||
      this._hasAllImagesSanitized()
    ) {
      // still use should-update-dimensions because text could also have updated
      this.is("should-update-dimensions", true);

      this.position();
      this.is("refreshing", false);
    } else {
      // mark as unsanitized so we sanitize again even after a hide
      this.is("sanitized", false);

      this._contentWrapper.css({ visibility: "hidden" });

      this.startLoading();

      this.sanitize(
        $.proxy(function() {
          this._contentWrapper.css({ visibility: "visible" });

          this.stopLoading();

          // set the update dimensions marker again since a position() call
          // on mousemove during refresh could have caused it to be unset
          this.is("should-update-dimensions", true);

          this.position();
          this.is("refreshing", false);
        }, this)
      );
    }
  },

  finishUpdate: function(callback) {
    this.is("updated", true);
    this.is("updating", false);

    if (typeof this.options.afterUpdate === "function") {
      // make sure visibility is visible during this
      var isHidden = this._contentWrapper.css("visibility");
      if (isHidden) this._contentWrapper.css({ visibility: "visible" });

      this.options.afterUpdate(this._content[0], this.element);

      if (isHidden) this._contentWrapper.css({ visibility: "hidden" });
    }

    if (callback) callback();
  }
});

$.extend(Tooltip.prototype, {
  clearUpdatedTo: function() {
    this._cache.updatedTo = {};
  },

  updateDimensionsToContent: function(targetPosition, stemPosition) {
    this.skin.build(); // skin has to be build at this point

    var isLoading = this.is("loading");
    var updatedTo = this._cache.updatedTo;

    if (
      !this._maxWidthPass &&
      !this.is("api") && // API calls always update
      //&& !this.is('refreshing')
      !this.is("should-update-dimensions") && // when this marker is set always update
      updatedTo.stemPosition === stemPosition &&
      updatedTo.loading === isLoading
    ) {
      return;
    }

    // always exit if we're loading and need to resize to content
    // the spinner will only change class at that point, while we stay
    // at old dimensions, so no need to do any further checks
    if (isLoading && this.is("resize-to-content")) {
      return;
    }

    // store so we can avoid duplicate updates
    this._cache.updatedTo = {
      type: this.is("resize-to-content") ? "content" : "spinner",
      loading: this.is("loading"),
      stemPosition: stemPosition
    };

    // if the should-update-dimensions flag was set
    // unset it since we're updating now
    if (this.is("should-update-dimensions")) {
      this.is("should-update-dimensions", false);
    }

    // actual updating from here
    targetPosition = targetPosition || this.options.position.target;
    stemPosition = stemPosition || this.options.position.tooltip;
    var side = Position.getSide(stemPosition);
    var orientation = Position.getOrientation(stemPosition);
    var border = this.skin._css.border;
    var borderPx = border + "px ";

    // set measure class before measuring
    this._tooltip.addClass("tpd-tooltip-measuring");
    var style = this._tooltip.attr("style");
    this._tooltip.removeAttr("style");

    var paddings = { top: border, right: border, bottom: border, left: border };

    // Add extra padding
    // if there's a stem and we're positioning to the side we might have to add some extra padding to the left
    var padding = 0;

    // if the stem orientation is vertical we might have to add extra padding
    if (Position.getOrientation(stemPosition) === "vertical") {
      // change the padding of the active side that of stemHeight
      if (this.options.stem) {
        paddings[side] = this.skin[
          "stem_" + side
        ].getMath().dimensions.outside.height;
      }

      // seems like a cheesy way to fix the mouse correction problem, but it works!
      var room = this.getMouseRoom();
      if (room[Position._flip[side]]) {
        paddings[side] += room[Position._flip[side]];
      }

      var containmentLayout = this.getContainmentLayout(stemPosition);
      var paddingLine = this.getPaddingLine(targetPosition);
      var addPadding = false;

      // if one of the points is within the box no need to check for intersection
      if (
        Position.isPointWithinBoxLayout(
          paddingLine.x1,
          paddingLine.y1,
          containmentLayout
        ) ||
        Position.isPointWithinBoxLayout(
          paddingLine.x2,
          paddingLine.y2,
          containmentLayout
        )
      ) {
        addPadding = true;
      } else {
        var intersects = false;
        $.each(
          "top right bottom left".split(" "),
          $.proxy(function(i, s) {
            var line = this.getSideLine(containmentLayout, s);

            if (
              Position.intersectsLine(
                paddingLine.x1,
                paddingLine.y1,
                paddingLine.x2,
                paddingLine.y2,
                line.x1,
                line.y1,
                line.x2,
                line.y2
              )
            ) {
              addPadding = true;
              return false;
            }
          }, this)
        );
      }

      if (addPadding) {
        // now if the stem is on the right we should add padding to that same side
        // so do that for left as well
        if (side === "left") {
          padding = paddingLine.x1 - containmentLayout.position.left;
        } else {
          padding =
            containmentLayout.position.left +
            containmentLayout.dimensions.width -
            paddingLine.x1;
        }

        paddings[side] += padding;
      }
    }

    // there can be added offset that requires extra padding
    if (this.options.offset) {
      if (orientation === "vertical") {
        var offset = Position.adjustOffsetBasedOnPosition(
          this.options.offset,
          this.options.position.target,
          targetPosition
        );

        if (offset.x !== 0) {
          paddings.right += Math.abs(offset.x);
        }
      }
    }

    // same thing for containment padding
    var padding;
    if (
      this.options.containment &&
      (padding = this.options.containment.padding)
    ) {
      $.each(paddings, function(name, value) {
        paddings[name] += padding;
      });

      // corrections: whenever the stem is on the side remove containment padding there
      if (orientation === "vertical") {
        // left/right
        paddings[side === "left" ? "left" : "right"] -= padding;
      } else {
        // top/bottom
        paddings[side === "top" ? "top" : "bottom"] -= padding;
      }
    }

    var viewport = Bounds.viewport();

    var hasInnerClose = this.close && this.close !== "overlap" && !this.title;

    var innerCloseDimensions = { width: 0, height: 0 };
    if (hasInnerClose) {
      innerCloseDimensions = this._innerCloseDimensions || {
        width: this._inner_close.outerWidth(true),
        height: this._inner_close.outerHeight(true)
      };
      this._innerCloseDimensions = innerCloseDimensions;
    }

    this._contentRelativePadder.css({
      "padding-right": innerCloseDimensions.width
    });

    this._contentSpacer.css({
      width: viewport.width - paddings.left - paddings.right
    });

    // first measure the dimensions
    var contentDimensions = {
      width: this._content.innerWidth() + innerCloseDimensions.width,
      height: Math.max(
        this._content.innerHeight(),
        innerCloseDimensions.height || 0
      )
    };

    var titleDimensions = { width: 0, height: 0 };

    // add title height if title or closebutton
    if (this.title) {
      var closeDimensions = { width: 0, height: 0 };

      this._titleWrapper.add(this._titleSpacer).css({
        width: "auto",
        height: "auto"
      });

      // measure close dimensions
      if (this.close && this.close !== "overlap") {
        //  || this.title
        closeDimensions = {
          width: this._close.outerWidth(true),
          height: this._close.outerHeight(true)
        };
        this._close.hide();
      }

      // There is a problem when maxWidth is set but when the element inserted as content has a larger fixed width
      // the title will be measured using the smaller maxWidth but it'll appear inside a larger area, making it only partially filled
      // to avoid this we use the max of maxWidth and content width in the second pass
      if (
        this._maxWidthPass &&
        contentDimensions.width > this.options.maxWidth
      ) {
        this._titleRelative.css({ width: contentDimensions.width });
      }

      // set padding on the spacer
      this._titleRelativePadder.css({ "padding-right": closeDimensions.width });

      // measure title border bottom
      var titleBorderBottom = parseFloat(
        this._titleWrapper.css("border-bottom-width")
      );

      // title dimensions
      titleDimensions = {
        width: this.title ? this._titleWrapper.innerWidth() : 0,
        height: Math.max(
          this.title ? this._titleWrapper.innerHeight() + titleBorderBottom : 0,
          closeDimensions.height + titleBorderBottom
        )
      };

      // make responsive
      if (
        titleDimensions.width >
        viewport.width - paddings.left - paddings.right
      ) {
        titleDimensions.width = viewport.width - paddings.left - paddings.right;

        this._titleSpacer.css({
          width: titleDimensions.width // - closeDimensions.width
        });

        titleDimensions.height = Math.max(
          this.title ? this._titleWrapper.innerHeight() + titleBorderBottom : 0,
          closeDimensions.height + titleBorderBottom
        );
      }

      contentDimensions.width = Math.max(
        titleDimensions.width,
        contentDimensions.width
      );
      contentDimensions.height += titleDimensions.height;

      // fixate the height since we're measuring it below
      // using innerHeight here cause we don't want to increase by the border
      this._titleWrapper.css({
        height: Math.max(
          this.title ? this._titleWrapper.innerHeight() : 0,
          closeDimensions.height
        )
      });

      if (this.close) {
        this._close.show();
      }
    }

    if (this.options.stem) {
      // min width/height
      var wh = orientation === "vertical" ? "height" : "width";
      var stemMath = this.skin["stem_" + side].getMath();
      var stemZ = stemMath.outside.width + 2 * this.skin._css.radius;

      if (contentDimensions[wh] < stemZ) {
        contentDimensions[wh] = stemZ;
      }
    }

    this._contentSpacer.css({ width: contentDimensions.width });

    if (
      contentDimensions.height !==
      Math.max(this._content.innerHeight(), innerCloseDimensions.height) +
        (this.title ? this._titleRelative.outerHeight() : 0)
    ) {
      contentDimensions.width++;
    }

    if (!this.is("resize-to-content")) {
      contentDimensions = this.skin._css.spinner.dimensions;
    }

    this.setDimensions(contentDimensions);

    // reset the spacing to the correct one, that of the border
    paddings = { top: border, right: border, bottom: border, left: border };
    if (this.options.stem) {
      var stemSide = Position.getSide(stemPosition);
      paddings[
        stemSide
      ] = this.skin.stem_top.getMath().dimensions.outside.height;
    }

    this._contentSpacer.css({
      "margin-top": paddings.top,
      "margin-left": +paddings.left,
      width: contentDimensions.width
    });

    if (this.title || this.close) {
      // if there's no close button, still show it while measuring
      this._titleWrapper.css({
        height: this._titleWrapper.innerHeight(),
        width: contentDimensions.width
      });
    }

    this._tooltip.removeClass("tpd-tooltip-measuring");
    this._tooltip.attr("style", style);

    // maxWidth
    var relatives = this._contentRelative.add(this._titleRelative);
    if (
      this.options.maxWidth &&
      contentDimensions.width > this.options.maxWidth &&
      !this._maxWidthPass &&
      this.is("resize-to-content")
    ) {
      relatives.css({ width: this.options.maxWidth });
      this._maxWidthPass = true;
      this.updateDimensionsToContent(targetPosition, stemPosition);
      this._maxWidthPass = false;
      relatives.css({ width: "auto" });
    }
  },

  setDimensions: function(dimensions) {
    this.skin.setDimensions(dimensions);
  },

  // return how much space we have around the target within the containment
  getContainmentSpace: function(stemPosition, ignorePadding) {
    var containmentLayout = this.getContainmentLayout(
      stemPosition,
      ignorePadding
    );
    var targetLayout = this.getTargetLayout();

    var tpos = targetLayout.position,
      tdim = targetLayout.dimensions,
      cpos = containmentLayout.position,
      cdim = containmentLayout.dimensions;

    var space = {
      top: Math.max(tpos.top - cpos.top, 0),
      bottom: Math.max(cpos.top + cdim.height - (tpos.top + tdim.height), 0),
      left: Math.max(tpos.left - cpos.left, 0),
      right: Math.max(cpos.left + cdim.width - (tpos.left + tdim.width), 0)
    };

    // we might have to subtract some more
    if (tpos.top > cpos.top + cdim.height) {
      space.top -= tpos.top - (cpos.top + cdim.height);
    }
    if (tpos.top + tdim.height < cpos.top) {
      space.bottom -= cpos.top - (tpos.top + tdim.height);
    }
    if (
      tpos.left > cpos.left + cdim.width &&
      cpos.left + cdim.width >= tpos.left
    ) {
      space.left -= tpos.left - (cpos.left + cdim.width);
    }
    if (tpos.left + tdim.width < cpos.left) {
      space.right -= cpos.left - (tpos.left + tdim.width);
    }

    this._cache.layouts.containmentSpace = space;

    return space;
  },

  position: function(event) {
    // this function could be called on mousemove with target: 'mouse',
    // prevent repositioning while the tooltip isn't visible yet / unattached
    // it will be positioned initially by show()
    if (!this.visible()) {
      return;
    }

    this.is("positioning", true);

    // first clear the layouts cache, otherwise we might be working with cached positions/dimensions/layouts
    this._cache.layouts = {};

    var _d = this._cache.dimensions; // for onResize callback

    var initialTargetPosition = this.options.position.target,
      initialStemPosition = this.options.position.tooltip,
      stemPosition = initialStemPosition,
      targetPosition = initialTargetPosition;

    this.updateDimensionsToContent(targetPosition, stemPosition);

    var initialPosition = this.getPositionBasedOnTarget(
      targetPosition,
      stemPosition
    );
    var position = deepExtend(initialPosition);

    var results = [];

    if (this.options.containment) {
      var newPositions = {};
      // check if at least one side is contained
      var oneSideContained = false;
      var containmentSides = {};
      $.each(
        "top right bottom left".split(" "),
        $.proxy(function(i, side) {
          if (
            (containmentSides[side] = this.isSideWithinContainment(
              side,
              stemPosition,
              true
            ))
          ) {
            // true ignored padding
            oneSideContained = true;
          }
        }, this)
      );

      // if no side is contained, fake a containment so we instantly position based on initial position
      if (!oneSideContained) {
        position.contained = true;
      }

      if (position.contained) {
        // if no side is contained we can just use this initial position as a fallback
        this.setPosition(position);
      } else {
        // store previous result
        results.unshift({
          position: position,
          targetPosition: targetPosition,
          stemPosition: stemPosition
        });

        // flip the target
        var inversedTarget = Position.flip(initialTargetPosition);
        targetPosition = inversedTarget;
        stemPosition = Position.flip(initialStemPosition);

        // if this side is contained we can try positioning it, otherwise fake uncontained
        if (containmentSides[Position.getSide(targetPosition)]) {
          this.updateDimensionsToContent(targetPosition, stemPosition);
          position = this.getPositionBasedOnTarget(
            targetPosition,
            stemPosition
          );
        } else {
          position.contained = false;
        }

        if (position.contained) {
          this.setPosition(position, stemPosition);
        } else {
          // store previous result
          results.unshift({
            position: position,
            targetPosition: targetPosition,
            stemPosition: stemPosition
          });

          // the origin point we'll be working with for the target is either the last set position or its initial position
          // this allows a tooltip that was previously flipped to become connected to the correct nearest point on the side
          var originTargetPosition = initialTargetPosition;

          // find out how much space we have on either side of the target
          // we ignore padding here since the passed in stemPosition here isn't the correct one, the one below is
          // what we want is just to figure out what has the most visible space within the containment area
          var space = this.getContainmentSpace(stemPosition, true); // ignore padding

          var newMinPos =
            Position.getOrientation(originTargetPosition) === "horizontal"
              ? ["left", "right"]
              : ["top", "bottom"];
          var newSide;
          if (space[newMinPos[0]] === space[newMinPos[1]]) {
            newSide =
              Position.getOrientation(originTargetPosition) === "horizontal"
                ? "left"
                : "top";
          } else {
            newSide =
              newMinPos[space[newMinPos[0]] > space[newMinPos[1]] ? 0 : 1];
          }

          var newCorner = Position.split(originTargetPosition)[1];
          var newTargetPosition = newSide + newCorner;

          var newStemPosition = Position.flip(newTargetPosition);

          targetPosition = newTargetPosition;
          stemPosition = newStemPosition;

          // if this side is contained we can try positioning it, otherwise fake uncontained
          if (containmentSides[Position.getSide(targetPosition)]) {
            this.updateDimensionsToContent(targetPosition, stemPosition);
            position = this.getPositionBasedOnTarget(
              targetPosition,
              stemPosition
            );
          } else {
            position.contained = false;
          }

          if (position.contained) {
            this.setPosition(position, stemPosition);
          } else {
            // store previous result
            results.unshift({
              position: position,
              targetPosition: targetPosition,
              stemPosition: stemPosition
            });

            // the fallback should be the result with the least negative positions
            var fallback;

            // since the array is reversed using unshift we start at the last position working back
            var negatives = [];
            $.each(results, function(i, result) {
              if (result.position.top >= 0 && result.position.left >= 0) {
                fallback = result;
              } else {
                // measure negativity based on how large the negative area is
                var ntop =
                    result.position.top >= 0
                      ? 1
                      : Math.abs(result.position.top),
                  nleft =
                    result.position.left >= 0
                      ? 1
                      : Math.abs(result.position.left);

                negatives.push({ result: result, negativity: ntop * nleft });
              }
            });

            // if we haven't found a fallback yet, go through the negatives to find the least negative one
            if (!fallback) {
              // start with the initial position
              var leastNegative = negatives[negatives.length - 1];

              // check all others to see if we can find a better one
              $.each(negatives, function(i, negative) {
                if (negative.negativity < leastNegative.negativity) {
                  leastNegative = negative;
                }
              });

              fallback = leastNegative.result;
            }

            // fallback
            this.updateDimensionsToContent(
              fallback.targetPosition,
              fallback.stemPosition,
              true
            );
            this.setPosition(fallback.position, fallback.stemPosition);
          }
        }
      }
    } else {
      // now set the position
      this.setPosition(position);
    }

    // onResize, we keep track of the dimensions in cache here
    this._cache.dimensions = this.skin._vars.dimensions;

    this.skin.paint();

    this.is("positioning", false);
  },

  getPositionBasedOnTarget: function(targetPosition, stemPosition) {
    stemPosition = stemPosition || this.options.position.tooltip;

    var dimensions = this.getTargetDimensions();

    var connection = { left: 0, top: 0 },
      max = { left: 0, top: 0 };

    var side = Position.getSide(targetPosition);
    var skinVars = this.skin._vars;
    var frame = skinVars.frames[Position.getSide(stemPosition)];

    var orientation = Position.getOrientation(targetPosition);
    var split = Position.split(targetPosition);
    var half;

    if (orientation === "horizontal") {
      // top/bottom
      half = Math.floor(dimensions.width * 0.5);

      switch (split[2]) {
        case "left":
          max.left = half;
          break;
        case "middle":
          //connection.x = dimensions.width - half;
          connection.left = dimensions.width - half; // we could instead ceil here
          max.left = connection.left;
          break;
        case "right":
          connection.left = dimensions.width;
          max.left = dimensions.width - half;
          break;
      }

      if (split[1] === "bottom") {
        connection.top = dimensions.height;
        max.top = dimensions.height;
      }
    } else {
      // left/right
      half = Math.floor(dimensions.height * 0.5);

      switch (split[2]) {
        case "top":
          max.top = half;
          break;
        case "middle":
          connection.top = dimensions.height - half;
          max.top = connection.top;
          break;
        case "bottom":
          max.top = dimensions.height - half;
          connection.top = dimensions.height;
          break;
      }

      if (split[1] === "right") {
        connection.left = dimensions.width;
        max.left = dimensions.width;
      }
    }

    // Align actual tooltip
    var targetOffset = this.getTargetPosition();

    var target = $.extend({}, dimensions, {
      top: targetOffset.top,
      left: targetOffset.left,
      connection: connection,
      max: max
    });

    var tooltip = {
      width: frame.dimensions.width,
      height: frame.dimensions.height,
      top: 0,
      left: 0,
      connection: skinVars.connections[stemPosition].connection,
      stem: skinVars.connections[stemPosition].stem
    };

    // Align the tooltip
    // first move the top/left to the connection of the target
    tooltip.top = target.top + target.connection.top;
    tooltip.left = target.left + target.connection.left;

    // now move it back by the connection on the tooltip to align it
    tooltip.top -= tooltip.connection.top;
    tooltip.left -= tooltip.connection.left;

    // now find out if the stem is within connection
    if (this.options.stem) {
      var stemWidth = skinVars.stemDimensions.width;
      var positions = {
        stem: {
          top: tooltip.top + tooltip.stem.connection.top,
          left: tooltip.left + tooltip.stem.connection.left
        },
        connection: {
          top: target.top + target.connection.top,
          left: target.left + target.connection.left
        },
        max: {
          top: target.top + target.max.top,
          left: target.left + target.max.left
        }
      };

      if (
        !Position.isPointWithinBox(
          positions.stem.left,
          positions.stem.top,
          positions.connection.left,
          positions.connection.top,
          positions.max.left,
          positions.max.top
        )
      ) {
        // align the stem with the nearest connection point on the target
        var positions = {
          stem: {
            top: tooltip.top + tooltip.stem.connection.top,
            left: tooltip.left + tooltip.stem.connection.left
          },
          connection: {
            top: target.top + target.connection.top,
            left: target.left + target.connection.left
          },
          max: {
            top: target.top + target.max.top,
            left: target.left + target.max.left
          }
        };

        var distances = {
          connection: Position.getDistance(
            positions.stem.left,
            positions.stem.top,
            positions.connection.left,
            positions.connection.top
          ),
          max: Position.getDistance(
            positions.stem.left,
            positions.stem.top,
            positions.max.left,
            positions.max.top
          )
        };

        // closest distance
        var distance = Math.min(distances.connection, distances.max);
        var closest =
          positions[
            distances.connection <= distances.max ? "connection" : "max"
          ];

        // find out on which axis the distance is
        var axis =
          Position.getOrientation(stemPosition) === "horizontal"
            ? "left"
            : "top";
        var closestToMax = Position.getDistance(
          positions.connection.left,
          positions.connection.top,
          positions.max.left,
          positions.max.top
        );

        if (stemWidth <= closestToMax) {
          // shift normally
          var moveToClosest = { top: 0, left: 0 };

          // the movement is on the corresponding axis, by a negative or positive margin, so inverse when needed
          var inversed = closest[axis] < positions.stem[axis] ? -1 : 1;
          moveToClosest[axis] = distance * inversed;

          // can we access stem width here
          moveToClosest[axis] += Math.floor(stemWidth * 0.5) * inversed;

          tooltip.left += moveToClosest.left;
          tooltip.top += moveToClosest.top;
        } else {
          // shift to center which is either closest or max
          // to find out which we calculate the distance to the center of the target
          $.extend(positions, {
            center: {
              top: Math.round(target.top + dimensions.height * 0.5),
              left: Math.round(target.left + dimensions.left * 0.5)
            }
          });

          var distancesToCenter = {
            connection: Position.getDistance(
              positions.center.left,
              positions.center.top,
              positions.connection.left,
              positions.connection.top
            ),
            max: Position.getDistance(
              positions.center.left,
              positions.center.top,
              positions.max.left,
              positions.max.top
            )
          };

          var distance =
            distances[
              distancesToCenter.connection <= distancesToCenter.max
                ? "connection"
                : "max"
            ];

          var moveToCenter = { top: 0, left: 0 };

          // the movement is on the corresponding axis, by a negative or positive margin, so inverse when needed
          var inversed = closest[axis] < positions.stem[axis] ? -1 : 1;
          moveToCenter[axis] = distance * inversed;

          tooltip.left += moveToCenter.left;
          tooltip.top += moveToCenter.top;
        }
      }
    }

    // now add offset
    if (this.options.offset) {
      var offset = $.extend({}, this.options.offset);
      offset = Position.adjustOffsetBasedOnPosition(
        offset,
        this.options.position.target,
        targetPosition
      );
      tooltip.top += offset.y;
      tooltip.left += offset.x;
    }

    // store a containment
    var containment = this.getContainment(
      {
        top: tooltip.top,
        left: tooltip.left
      },
      stemPosition
    );

    var contained = containment.horizontal && containment.vertical;
    var shift = { x: 0, y: 0 }; // movement of the stem

    // we can only correct the stem on the tooltip, so check for containment in its orientation
    var stemOrientation = Position.getOrientation(stemPosition);

    if (!containment[stemOrientation]) {
      var isHorizontalStem = stemOrientation === "horizontal",
        movements = isHorizontalStem ? ["left", "right"] : ["up", "down"],
        correctionDirection = isHorizontalStem ? "x" : "y",
        correctionDirectionTL = isHorizontalStem ? "left" : "top",
        correctPx = containment.correction[correctionDirection];

      // we need to find the negative space threshold
      var containmentLayout = this.getContainmentLayout(stemPosition),
        negativeSpaceThreshold =
          containmentLayout.position[isHorizontalStem ? "left" : "top"]; // used to be 0 but containment padding makes this variable

      if (correctPx !== 0) {
        // we can't correct by 0
        var allowedShift = skinVars.connections[stemPosition].move;
        var allowedShiftPx =
          allowedShift[movements[correctPx * -1 < 0 ? 0 : 1]];
        var multiplier = correctPx < 0 ? -1 : 1;

        if (
          allowedShiftPx >= correctPx * multiplier && // when enough allowed movement
          tooltip[correctionDirectionTL] + correctPx >= negativeSpaceThreshold
        ) {
          tooltip[correctionDirectionTL] += correctPx;
          shift[correctionDirection] = correctPx * -1;

          // now mark as contained
          contained = true;
        } else if (
          Position.getOrientation(targetPosition) ===
          Position.getOrientation(stemPosition)
        ) {
          // STEM + TOOLTIP SHIFT:
          // max shift
          tooltip[correctionDirectionTL] += allowedShiftPx * multiplier;
          shift[correctionDirection] = allowedShiftPx * multiplier * -1;

          // if we've moved into negative space we should apply a correction
          if (tooltip[correctionDirectionTL] < negativeSpaceThreshold) {
            var backPx =
              negativeSpaceThreshold - tooltip[correctionDirectionTL];

            // movement back should still be in allowed range
            // we can't move further back than the tooltip allows in total shift
            var maxBackPx =
              allowedShift[movements[0]] + allowedShift[movements[1]];
            backPx = Math.min(backPx, maxBackPx);

            // only shift the tooltip since it's already maxed out on stem shift
            tooltip[correctionDirectionTL] += backPx;

            // we can shift the stem but not beyond the maximum
            var shiftStemResult = shift[correctionDirection] - backPx;
            if (
              shiftStemResult >=
                skinVars.connections[stemPosition].move[movements[0]] &&
              shiftStemResult <=
                skinVars.connections[stemPosition].move[movements[1]]
            ) {
              shift[correctionDirection] -= backPx; // needed because otherwise the correction on the tooltip could make the stem seem disconnected
            }
          }

          // adjust containment so we can get the correct remaining correction
          containment = this.getContainment(
            {
              top: tooltip.top,
              left: tooltip.left
            },
            stemPosition
          );

          var stillRequiredPx = containment.correction[correctionDirection];

          // for these calculations we require the tooltip position without offset, adding it back later on
          var tooltipWithoutOffset = deepExtend({}, tooltip);

          if (this.options.offset) {
            tooltipWithoutOffset.left -= this.options.offset.x;
            tooltipWithoutOffset.top -= this.options.offset.y;
          }

          var positions = {
            stem: {
              top: tooltipWithoutOffset.top + tooltip.stem.connection.top,
              left: tooltipWithoutOffset.left + tooltip.stem.connection.left
            }
          };
          positions.stem[correctionDirectionTL] += shift[correctionDirection];

          var targetLayout = this.getTargetLayout();
          var stemWidth = skinVars.stemDimensions.width;
          var halfStemWidth = Math.floor(stemWidth * 0.5);
          // the containment threshold on the other side of the negative threshold, we call it positive but it's also sort of a negative
          // used to measure overflow on the right/bottom
          var positiveSpaceThreshold =
            negativeSpaceThreshold +
            containmentLayout.dimensions[isHorizontalStem ? "width" : "height"];

          if (correctionDirection === "x") {
            // find possible x
            var x = targetLayout.position.left + halfStemWidth;
            if (stillRequiredPx > 0) {
              // find leftmost
              x += targetLayout.dimensions.width - halfStemWidth * 2;
            }

            // move the tooltip and stem as far as needed and possible.
            // as far as what's possible on the stem, we allow movement from half a stem width to half a stem width on the other end of the tooltip.
            // the last check on each line is a guard against containment padding.
            if (
              (stillRequiredPx < 0 &&
                positions.stem.left + stillRequiredPx >= x &&
                tooltipWithoutOffset.left + stillRequiredPx >=
                  negativeSpaceThreshold) || // left
              (stillRequiredPx > 0 &&
                positions.stem.left + stillRequiredPx <= x &&
                tooltipWithoutOffset.left + stillRequiredPx <=
                  positiveSpaceThreshold) // right
            ) {
              tooltipWithoutOffset.left += stillRequiredPx;
            }
          } else {
            // possible y
            var y = targetLayout.position.top + halfStemWidth;
            if (stillRequiredPx > 0) {
              // find leftmost
              y += targetLayout.dimensions.height - halfStemWidth * 2;
            }

            // now see if the adjustment is possible
            if (
              (stillRequiredPx < 0 &&
                positions.stem.top + stillRequiredPx >= y &&
                tooltipWithoutOffset.top + stillRequiredPx >=
                  negativeSpaceThreshold) || // top
              (stillRequiredPx > 0 &&
                positions.stem.top + stillRequiredPx <= y &&
                tooltipWithoutOffset.top + stillRequiredPx <=
                  positiveSpaceThreshold) // bottom
            ) {
              tooltipWithoutOffset.top += stillRequiredPx;
            }
          }

          // add back the offset
          tooltip = tooltipWithoutOffset;
          if (this.options.offset) {
            tooltip.left += this.options.offset.x;
            tooltip.top += this.options.offset.y;
          }
        }
      }

      // now it could be that we've moved the tooltip into containment on one side but out of it in the other, to check against this, adjust again
      containment = this.getContainment(
        { top: tooltip.top, left: tooltip.left },
        stemPosition
      );
      contained = containment.horizontal && containment.vertical;
    }

    return {
      top: tooltip.top,
      left: tooltip.left,
      contained: contained,
      shift: shift
    };
  },

  setPosition: function(position, stemPosition) {
    var _p = this._position;

    if (!(_p && _p.top === position.top && _p.left === position.left)) {
      // handle a different container
      var container;
      if (this.options.container !== document.body) {
        if ( typeof this.options.container === "string") {
          var target = this.target;
          if (target === "mouse") {
            target = this.element;
          }

          container = $(
            $(target)
              .closest(this.options.container)
              .first()
          );
        } else {
          container = $(container);
        }

        // we default to document body, if nothing was found
        // so only use this is we actually have an element
        if (container[0]) {
          var _offset = $(container).offset(),
            offset = {
              top: Math.round(_offset.top),
              left: Math.round(_offset.left)
            },
            scroll = {
              top: Math.round($(container).scrollTop()),
              left: Math.round($(container).scrollLeft())
            };

          position.top -= offset.top;
          position.top += scroll.top;
          position.left -= offset.left;
          position.left += scroll.left;
        }
      }

      this._position = position;

      this._tooltip.css({
        top: position.top,
        left: position.left
      });
    }

    this.skin.setStemPosition(
      stemPosition || this.options.position.tooltip,
      position.shift || { x: 0, y: 0 }
    );
  },

  getSideLine: function(layout, side) {
    var x1 = layout.position.left,
      y1 = layout.position.top,
      x2 = layout.position.left,
      y2 = layout.position.top;

    switch (side) {
      case "top":
        x2 += layout.dimensions.width;
        break;
      case "bottom":
        y1 += layout.dimensions.height;
        x2 += layout.dimensions.width;
        y2 += layout.dimensions.height;
        break;
      case "left":
        y2 += layout.dimensions.height;
        break;
      case "right":
        x1 += layout.dimensions.width;
        x2 += layout.dimensions.width;
        y2 += layout.dimensions.height;
        break;
    }

    return { x1: x1, y1: y1, x2: x2, y2: y2 };
  },

  isSideWithinContainment: function(targetSide, stemPosition, ignorePadding) {
    var containmentLayout = this.getContainmentLayout(
      stemPosition,
      ignorePadding
    );
    var targetLayout = this.getTargetLayout();
    var isWithin = false;

    var targetLine = this.getSideLine(targetLayout, targetSide);

    // if one of the points is within the box we can return true right away
    if (
      Position.isPointWithinBoxLayout(
        targetLine.x1,
        targetLine.y1,
        containmentLayout
      ) ||
      Position.isPointWithinBoxLayout(
        targetLine.x2,
        targetLine.y2,
        containmentLayout
      )
    ) {
      return true;
    } else {
      // the box forming the target might better bigger than the containment area
      // so we should check if the line forming the sides intersects with one of the lines forming the containment area
      var intersects = false;
      $.each(
        "top right bottom left".split(" "),
        $.proxy(function(i, s) {
          var line = this.getSideLine(containmentLayout, s);

          if (
            Position.intersectsLine(
              targetLine.x1,
              targetLine.y1,
              targetLine.x2,
              targetLine.y2,
              line.x1,
              line.y1,
              line.x2,
              line.y2
            )
          ) {
            intersects = true;
            return false;
          }
        }, this)
      );

      return intersects;
    }
  },

  getContainment: function(position, stemPosition) {
    var contained = {
      horizontal: true,
      vertical: true,
      correction: { y: 0, x: 0 }
    };

    if (this.options.containment) {
      var containmentLayout = this.getContainmentLayout(stemPosition);

      var dimensions = this.skin._vars.frames[Position.getSide(stemPosition)]
        .dimensions;

      if (this.options.containment) {
        if (
          position.left < containmentLayout.position.left ||
          position.left + dimensions.width >
            containmentLayout.position.left + containmentLayout.dimensions.width
        ) {
          contained.horizontal = false;

          // store the correction that would be required
          if (position.left < containmentLayout.position.left) {
            contained.correction.x =
              containmentLayout.position.left - position.left;
          } else {
            contained.correction.x =
              containmentLayout.position.left +
              containmentLayout.dimensions.width -
              (position.left + dimensions.width);
          }
        }
        if (
          position.top < containmentLayout.position.top ||
          position.top + dimensions.height >
            containmentLayout.position.top + containmentLayout.dimensions.height
        ) {
          contained.vertical = false;

          // store the correction that would be required
          if (position.top < containmentLayout.position.top) {
            contained.correction.y =
              containmentLayout.position.top - position.top;
          } else {
            contained.correction.y =
              containmentLayout.position.top +
              containmentLayout.dimensions.height -
              (position.top + dimensions.height);
          }
        }
      }
    }

    return contained;
  },

  // stemPosition is used here since it might change containment padding
  getContainmentLayout: function(stemPosition, ignorePadding) {
    var viewportScroll = {
      top: $(window).scrollTop(),
      left: $(window).scrollLeft()
    };

    var target = this.target;
    if (target === "mouse") {
      target = this.element;
    }

    var area = $(target)
      .closest(this.options.containment.selector)
      .first()[0];

    var layout;

    if (!area || this.options.containment.selector === "viewport") {
      layout = {
        dimensions: Bounds.viewport(),
        position: viewportScroll
      };
    } else {
      layout = {
        dimensions: {
          width: $(area).innerWidth(),
          height: $(area).innerHeight()
        },
        position: $(area).offset()
      };
    }

    var padding = this.options.containment.padding;
    if (padding && !ignorePadding) {
      var maxDim = Math.max(layout.dimensions.height, layout.dimensions.width);
      if (padding * 2 > maxDim) {
        padding = Math.max(Math.floor(maxDim * 0.5), 0);
      }

      if (padding) {
        layout.dimensions.width -= 2 * padding;
        layout.dimensions.height -= 2 * padding;
        layout.position.top += padding;
        layout.position.left += padding;

        // when the stem is on the left/right we don't want the padding there so
        // the padding doesn't interfere with the stem once the viewport becomes smaller
        // we only want padding on one side in those situations
        var orientation = Position.getOrientation(stemPosition);

        // left/right
        if (orientation === "vertical") {
          layout.dimensions.width += padding;

          if (Position.getSide(stemPosition) === "left") {
            layout.position.left -= padding;
          }
        } else {
          // top/bottom
          layout.dimensions.height += padding;

          if (Position.getSide(stemPosition) === "top") {
            layout.position.top -= padding;
          }
        }
      }
    }

    this._cache.layouts.containmentLayout = layout;

    return layout;
  },

  // room top/bottom/left/right on the element compared to the mouse position
  getMouseRoom: function() {
    var room = {
      top: 0,
      left: 0,
      right: 0,
      bottom: 0
    };

    if (this.options.target === "mouse" && !this.is("api")) {
      var actualMousePosition = Mouse.getActualPosition(this._cache.event);
      var elementPosition = $(this.element).offset();
      var elementDimensions = {
        width: $(this.element).innerWidth(),
        height: $(this.element).innerHeight()
      };
      room = {
        top: Math.max(0, actualMousePosition.top - elementPosition.top),
        bottom: Math.max(
          0,
          elementPosition.top +
            elementDimensions.height -
            actualMousePosition.top
        ),
        left: Math.max(0, actualMousePosition.left - elementPosition.left),
        right: Math.max(
          0,
          elementPosition.left +
            elementDimensions.width -
            actualMousePosition.left
        )
      };
    }

    return room;
  },

  // Target layout
  getTargetPosition: function() {
    var position, offset;

    if (this.options.target === "mouse") {
      if (this.is("api")) {
        // when we've called this method from the API, use the element position instead
        offset = $(this.element).offset();
        position = {
          top: Math.round(offset.top),
          left: Math.round(offset.left)
        };
      } else {
        // mouse position is safe to use
        position = Mouse.getPosition(this._cache.event);
      }
    } else {
      offset = $(this.target).offset();
      position = {
        top: Math.round(offset.top),
        left: Math.round(offset.left)
      };
    }

    this._cache.layouts.targetPosition = position;

    return position;
  },
  getTargetDimensions: function() {
    if (this._cache.layouts.targetDimensions)
      return this._cache.layouts.targetDimensions;

    var dimensions;

    if (this.options.target === "mouse") {
      dimensions = Mouse.getDimensions();
    } else {
      dimensions = {
        width: $(this.target).innerWidth(),
        height: $(this.target).innerHeight()
      };
    }

    this._cache.layouts.targetDimensions = dimensions;

    return dimensions;
  },
  getTargetLayout: function() {
    if (this._cache.layouts.targetLayout)
      return this._cache.layouts.targetLayout;

    var layout = {
      position: this.getTargetPosition(),
      dimensions: this.getTargetDimensions()
    };

    this._cache.layouts.targetLayout = layout;

    return layout;
  },

  getPaddingLine: function(targetPosition) {
    var targetLayout = this.getTargetLayout();

    var side = "left";
    if (Position.getOrientation(targetPosition) === "vertical") {
      return this.getSideLine(targetLayout, Position.getSide(targetPosition));
    } else {
      if (Position.isCorner(targetPosition)) {
        var corner = Position.inverseCornerPlane(targetPosition);
        side = Position.getSide(corner);
        return this.getSideLine(targetLayout, side);
      } else {
        // middle top or bottom
        var line = this.getSideLine(targetLayout, side);

        // we have to add half the width to the lane for it to span the entire middle as a line
        var halfWidth = Math.round(targetLayout.dimensions.width * 0.5);
        line.x1 += halfWidth;
        line.x2 += halfWidth;

        return line;
      }
    }
  }
});

$.extend(Tooltip.prototype, {
  setActive: function() {
    this.is("active", true);

    //  raise the tooltip if it's visible
    if (this.visible()) {
      this.raise();
    }

    if (this.options.hideAfter) {
      this.clearTimer("idle");
    }
  },
  setIdle: function() {
    this.is("active", false);

    if (this.options.hideAfter) {
      this.setTimer(
        "idle",
        $.proxy(function() {
          this.clearTimer("idle");

          if (!this.is("active")) {
            this.hide();
          }
        }, this),
        this.options.hideAfter
      );
    }
  }
});

$.extend(Tooltip.prototype, {
  // bind with cached event to make unbinding cached handlers easy
  bind: function(element, eventName, handler, context) {
    var cachedHandler = $.proxy(handler, context || this);

    this._cache.events.push({
      element: element,
      eventName: eventName,
      handler: cachedHandler
    });

    $(element).on(eventName, cachedHandler);
  },

  unbind: function() {
    $.each(this._cache.events, function(i, event) {
      $(event.element).unbind(event.eventName, event.handler);
    });

    this._cache.events = [];
  }
});

$.extend(Tooltip.prototype, {
  disable: function() {
    if (this.is("disabled")) return;
    this.is("disabled", true);
  },

  enable: function() {
    if (!this.is("disabled")) return;
    this.is("disabled", false);
  }
});

$.extend(Tooltip.prototype, {
  // states
  is: function(question, answer) {
    if (typeof answer === "boolean") {
      this._cache.is[question] = answer;
    }

    return this._cache.is[question];
  },

  visible: function() {
    return this.is("visible");
  }
});

$.extend(Tooltip.prototype, {
  setTimer: function(name, handler, ms) {
    this._cache.timers[name] = _.delay(handler, ms);
  },

  getTimer: function(name) {
    return this._cache.timers[name];
  },

  clearTimer: function(name) {
    if (this._cache.timers[name]) {
      clearTimeout(this._cache.timers[name]);
      delete this._cache.timers[name];
    }
  },

  clearTimers: function() {
    $.each(this._cache.timers, function(i, timer) {
      clearTimeout(timer);
    });
    this._cache.timers = {};
  }
});

$.extend(Tipped, {
  init: function() {
    Tooltips.init();
  },

  create: function(element, content) {
    var options = $.extend({}, arguments[2] || {}),
      tooltips = [];

    // initialize tooltips
    if (_.isElement(element)) {
      tooltips.push(new Tooltip(element, content, options));
    } else {
      // assume selector
      $(element).each(function(i, el) {
        tooltips.push(new Tooltip(el, content, options));
      });
    }

    return new Collection(tooltips);
  },

  get: function(selector) {
    var tooltips = Tooltips.get(selector);
    return new Collection(tooltips);
  },

  findElement: function(element) {
    return Tooltips.findElement(element);
  },

  hideAll: function() {
    Tooltips.hideAll();
    return this;
  },

  setDefaultSkin: function(name) {
    Tooltips.setDefaultSkin(name);
    return this;
  },

  visible: function(selector) {
    if (_.isElement(selector)) {
      return Tooltips.isVisibleByElement(selector);
    } else if ( typeof selector !== "undefined") {
      var elements = $(selector),
        visible = 0;
      $.each(elements, function(i, element) {
        if (Tooltips.isVisibleByElement(element)) visible++;
      });
      return visible;
    } else {
      return Tooltips.getVisible().length;
    }
  },

  clearAjaxCache: function() {
    Tooltips.clearAjaxCache();
    return this;
  },

  refresh: function(selector, doneCallback, progressCallback) {
    Tooltips.refresh(selector, doneCallback, progressCallback);
    return this;
  },

  setStartingZIndex: function(index) {
    Tooltips.setStartingZIndex(index);
    return this;
  },

  remove: function(selector) {
    Tooltips.remove(selector);
    return this;
  }
});

$.each("show hide toggle disable enable".split(" "), function(i, name) {
  Tipped[name] = function(selector) {
    this.get(selector)[name]();
    return this;
  };
});

$.extend(Tipped, {
  delegate: function() {
    Delegations.add.apply(Delegations, _slice.call(arguments));
  },

  undelegate: function() {
    Delegations.remove.apply(Delegations, _slice.call(arguments));
  }
});

var Delegations = {
  _uid: 0,
  _delegations: {},

  add: function(selector, content, options) {
    var options;
    if ( typeof content === "object" && !_.isElement(content)) {
      options = content;
      content = null;
    } else {
      options = arguments[2] || {};
    }

    var uid = ++this._uid;

    var ttOptions = Options.create($.extend({}, options));

    this._delegations[uid] = {
      uid: uid,
      selector: selector,
      content: content,
      options: ttOptions
    };

    var handler = function(event) {
      // store the uid so we don't create a second tooltip
      $(this).addClass("tpd-delegation-uid-" + uid);

      // now create the tooltip
      var tooltip = new Tooltip(this, content, options);

      // store any cached pageX/Y on it
      tooltip._cache.event = event;

      tooltip.setActive();

      tooltip.showDelayed();
    };

    this._delegations[uid].removeTitleHandler = $.proxy(this.removeTitle, this);
    $(document).on(
      "mouseenter",
      selector + ":not(.tpd-delegation-uid-" + uid + ")",
      this._delegations[uid].removeTitleHandler
    );

    this._delegations[uid].handler = handler;
    $(document).on(
      ttOptions.showOn.element,
      selector + ":not(.tpd-delegation-uid-" + uid + ")",
      handler
    );
  },

  // puts the title into data-tipped-restore-title,
  // this way tooltip creation picks up on it
  // without showing the native title tooltip
  removeTitle: function(event) {
    var element = event.currentTarget;

    var title = $(element).attr("title");

    // backup title
    if (title) {
      $(element).data("tipped-restore-title", title);
      $(element)[0].setAttribute("title", ""); // IE needs setAttribute
    }
  },

  remove: function(selector) {
    $.each(
      this._delegations,
      $.proxy(function(uid, delegation) {
        if (delegation.selector === selector) {
          $(document)
            .undelegate(
              selector + ":not(.tpd-delegation-uid-" + uid + ")",
              "mouseenter",
              delegation.removeTitleHandler
            )
            .undelegate(
              selector + ":not(.tpd-delegation-uid-" + uid + ")",
              delegation.options.showOn.element,
              delegation.handler
            );
          delete this._delegations[uid];
        }
      }, this)
    );
  },

  removeAll: function() {
    $.each(
      this._delegations,
      $.proxy(function(uid, delegation) {
        $(document)
          .undelegate(
            delegation.selector + ":not(.tpd-delegation-uid-" + uid + ")",
            "mouseenter",
            delegation.removeTitleHandler
          )
          .undelegate(
            delegation.selector + ":not(.tpd-delegation-uid-" + uid + ")",
            delegation.options.showOn.element,
            delegation.handler
          );
        delete this._delegations[uid];
      }, this)
    );
  }
};

function Collection() {
  this.initialize.apply(this, _slice.call(arguments));
}

$.extend(Collection.prototype, {
  initialize: function(tooltips) {
    this.tooltips = tooltips;
    return this;
  },

  items: function() {
    // everytime we grab a tooltip collection we'll clear the mouse buffer
    // this way it's never passed onto the elements
    $.each(this.tooltips, function(i, tooltip) {
      tooltip.is("api", true);
    });

    return this.tooltips;
  },

  refresh: function(callback) {
    $.each(this._tooltips, function(i, tooltip) {
      if (tooltip.is("visible")) {
        tooltip.refresh();
      }
    });
    return this;
  },

  remove: function() {
    Tooltips.removeTooltips(this.tooltips);

    // clear tooltips on this collection
    this.tooltips = [];

    return this;
  }
});

$.each("show hide toggle disable enable".split(" "), function(i, name) {
  Collection.prototype[name] = function() {
    $.each(this.tooltips, function(j, tooltip) {
      tooltip.is("api", true);
      tooltip[name]();
    });
    return this;
  };
});

Tipped.init();

return Tipped;

}));