Created
March 25, 2020 11:37
-
-
Save Arsenije/59a4beb4189b56cea6bb039ee76df411 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/*! | |
* Tipped - A Complete Javascript Tooltip Solution - v4.7.0 | |
* (c) 2012-2019 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.0' | |
}); | |
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 && $.type(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 = $.type(elements) == '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 = $.type(arguments[1]), | |
options = argTypeOne === "object" ? arguments[1] : {}, | |
callback = | |
argTypeOne === "function" | |
? arguments[1] | |
: $.type(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 = | |
$.type(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 ($.type(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 ($.type(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 ($.type(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) || | |
($.type(MERGED.position) === "string" && MERGED.position) || | |
(RESET.position && RESET.position.target) || | |
($.type(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 ($.type(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 ($.type(showOn) === "string") { | |
showOn = { element: showOn }; | |
} | |
MERGED.showOn = showOn; | |
} | |
if (MERGED.hideOn) { | |
var hideOn = MERGED.hideOn; | |
if ($.type(hideOn) === "string") { | |
hideOn = { element: hideOn }; | |
} | |
MERGED.hideOn = hideOn; | |
} | |
// normalize inline | |
if (MERGED.inline) { | |
if ($.type(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 ($.type(MERGED.spinner) === "boolean") { | |
MERGED.spinner = RESET.spinner || BASE.spinner || {}; | |
} | |
} | |
} | |
if (!MERGED.container) { | |
MERGED.container = document.body; | |
} | |
if (MERGED.containment) { | |
if ($.type(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).bind("resize orientationchange", this._resizeHandler); | |
if (Browser.MobileSafari) { | |
$("body").bind("click", this._emptyClickHandler); | |
} | |
}, | |
reset: function() { | |
Tooltips.removeAll(); | |
Delegations.removeAll(); | |
if (this._resizeHandler) { | |
$(window).unbind("resize orientationchange", this._resizeHandler); | |
} | |
if (Browser.MobileSafari) { | |
$("body").unbind("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 ($.type(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 ( | |
$.type(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 ($.type(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 ($.type(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 ($.type(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("×") | |
)) | |
)) | |
) | |
.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("×") | |
)) | |
)) | |
)) | |
)) | |
); | |
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.delegate( | |
".tpd-close, .close-tooltip", | |
"click", | |
$.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") && $.type(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 = | |
($.type(duration) === "number" ? duration : this.options.fadeIn) || 0; | |
callback = | |
callback || ($.type(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 ($.type(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 ($.type(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 || ($.type(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 = $.type(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 ($.type(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 ( | |
$.type(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 && $.type(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 ($.type(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 ($.type(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).bind(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 ($.type(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 ($.type(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 ($.type(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).delegate( | |
selector + ":not(.tpd-delegation-uid-" + uid + ")", | |
"mouseenter", | |
this._delegations[uid].removeTitleHandler | |
); | |
this._delegations[uid].handler = handler; | |
$(document).delegate( | |
selector + ":not(.tpd-delegation-uid-" + uid + ")", | |
ttOptions.showOn.element, | |
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; | |
})); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment