diff --git a/PlexRequests.UI/Content/base.css b/PlexRequests.UI/Content/base.css
index 0a322e81c..f9301d689 100644
--- a/PlexRequests.UI/Content/base.css
+++ b/PlexRequests.UI/Content/base.css
@@ -333,3 +333,6 @@ label {
margin-top: 0 !important;
margin-bottom: 0 !important; }
+.tooltip_templates {
+ display: none; }
+
diff --git a/PlexRequests.UI/Content/base.min.css b/PlexRequests.UI/Content/base.min.css
index de31f3f8e..04676742b 100644
--- a/PlexRequests.UI/Content/base.min.css
+++ b/PlexRequests.UI/Content/base.min.css
@@ -1 +1 @@
-@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}.landing-block .media{max-width:450px;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;}hr{border:1px dashed #777;}.btn{border-radius:.25rem !important;}.btn-group-separated .btn,.btn-group-separated .btn+.btn{margin-left:3px;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.nav-tabs>li{font-size:13px;line-height:21px;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.nav-tabs>li>a>.fa{padding:3px 5px 3px 3px;}.nav-tabs>li.nav-tab-right{float:right;}.nav-tabs>li.nav-tab-right a{margin-right:0;margin-left:2px;}.nav-tabs>li.nav-tab-icononly .fa{padding:3px;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:10px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}#updateAvailable{background-color:#df691a;text-align:center;font-size:15px;padding:3px 0;}.checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:3px;}.checkbox input[type=checkbox]{display:none;}.checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#4e5d6c;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #4e5d6c !important;}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{color:#fff !important;}.landing-header{display:block;margin:60px auto;}.landing-block{background:#2f2f2f !important;padding:5px;}.landing-block .media{margin:30px auto;max-width:450px;}.landing-block .media .media-left{display:inline-block;float:left;width:70px;}.landing-block .media .media-left i.fa{font-size:3em;}.landing-title{font-weight:bold;}.checkbox-custom{margin-top:0 !important;margin-bottom:0 !important;}
\ No newline at end of file
+@media(min-width:768px){.row{position:relative;}.bottom-align-text{position:absolute;bottom:0;right:0;}.landing-block .media{max-width:450px;}}@media(max-width:48em){.home{padding-top:1rem;}}@media(min-width:48em){.home{padding-top:4rem;}}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#fff;}hr{border:1px dashed #777;}.btn{border-radius:.25rem !important;}.btn-group-separated .btn,.btn-group-separated .btn+.btn{margin-left:3px;}.multiSelect{background-color:#4e5d6c;}.form-control-custom{background-color:#4e5d6c !important;color:#fff !important;border-radius:0;box-shadow:0 0 0 !important;}h1{font-size:3.5rem !important;font-weight:600 !important;}.request-title{margin-top:0 !important;font-size:1.9rem !important;}p{font-size:1.1rem !important;}label{display:inline-block !important;margin-bottom:.5rem !important;font-size:16px !important;}.nav-tabs>li{font-size:13px;line-height:21px;}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{background:#4e5d6c;}.nav-tabs>li>a>.fa{padding:3px 5px 3px 3px;}.nav-tabs>li.nav-tab-right{float:right;}.nav-tabs>li.nav-tab-right a{margin-right:0;margin-left:2px;}.nav-tabs>li.nav-tab-icononly .fa{padding:3px;}.navbar .nav a .fa,.dropdown-menu a .fa{font-size:130%;top:1px;position:relative;display:inline-block;margin-right:5px;}.dropdown-menu a .fa{top:2px;}.btn-danger-outline{color:#d9534f !important;background-color:transparent;background-image:none;border-color:#d9534f !important;}.btn-danger-outline:focus,.btn-danger-outline.focus,.btn-danger-outline:active,.btn-danger-outline.active,.btn-danger-outline:hover,.open>.btn-danger-outline.dropdown-toggle{color:#fff !important;background-color:#d9534f !important;border-color:#d9534f !important;}.btn-primary-outline{color:#ff761b !important;background-color:transparent;background-image:none;border-color:#ff761b !important;}.btn-primary-outline:focus,.btn-primary-outline.focus,.btn-primary-outline:active,.btn-primary-outline.active,.btn-primary-outline:hover,.open>.btn-primary-outline.dropdown-toggle{color:#fff !important;background-color:#df691a !important;border-color:#df691a !important;}.btn-info-outline{color:#5bc0de !important;background-color:transparent;background-image:none;border-color:#5bc0de !important;}.btn-info-outline:focus,.btn-info-outline.focus,.btn-info-outline:active,.btn-info-outline.active,.btn-info-outline:hover,.open>.btn-info-outline.dropdown-toggle{color:#fff !important;background-color:#5bc0de !important;border-color:#5bc0de !important;}.btn-warning-outline{color:#f0ad4e !important;background-color:transparent;background-image:none;border-color:#f0ad4e !important;}.btn-warning-outline:focus,.btn-warning-outline.focus,.btn-warning-outline:active,.btn-warning-outline.active,.btn-warning-outline:hover,.open>.btn-warning-outline.dropdown-toggle{color:#fff !important;background-color:#f0ad4e !important;border-color:#f0ad4e !important;}.btn-success-outline{color:#5cb85c !important;background-color:transparent;background-image:none;border-color:#5cb85c !important;}.btn-success-outline:focus,.btn-success-outline.focus,.btn-success-outline:active,.btn-success-outline.active,.btn-success-outline:hover,.open>.btn-success-outline.dropdown-toggle{color:#fff !important;background-color:#5cb85c !important;border-color:#5cb85c !important;}#movieList .mix{display:none;}#tvList .mix{display:none;}.scroll-top-wrapper{position:fixed;opacity:0;visibility:hidden;overflow:hidden;text-align:center;z-index:99999999;background-color:#4e5d6c;color:#eee;width:50px;height:48px;line-height:48px;right:30px;bottom:30px;padding-top:2px;border-top-left-radius:10px;border-top-right-radius:10px;border-bottom-right-radius:10px;border-bottom-left-radius:10px;-webkit-transition:all .5s ease-in-out;-moz-transition:all .5s ease-in-out;-ms-transition:all .5s ease-in-out;-o-transition:all .5s ease-in-out;transition:all .5s ease-in-out;}.scroll-top-wrapper:hover{background-color:#637689;}.scroll-top-wrapper.show{visibility:visible;cursor:pointer;opacity:1;}.scroll-top-wrapper i.fa{line-height:inherit;}.no-search-results{text-align:center;}.no-search-results .no-search-results-icon{font-size:10em;color:#4e5d6c;}.no-search-results .no-search-results-text{margin:20px 0;color:#ccc;}.form-control-search{padding:13px 105px 13px 16px;height:100%;}.form-control-withbuttons{padding-right:105px;}.input-group-addon .btn-group{position:absolute;right:45px;z-index:3;top:10px;box-shadow:0 0 0;}.input-group-addon .btn-group .btn{border:1px solid rgba(255,255,255,.7) !important;padding:3px 12px;color:rgba(255,255,255,.7) !important;}.btn-split .btn{border-radius:0 !important;}.btn-split .btn:not(.dropdown-toggle){border-radius:.25rem 0 0 .25rem !important;}.btn-split .btn.dropdown-toggle{border-radius:0 .25rem .25rem 0 !important;padding:12px 8px;}#updateAvailable{background-color:#df691a;text-align:center;font-size:15px;padding:3px 0;}.checkbox label{display:inline-block;cursor:pointer;position:relative;padding-left:25px;margin-right:15px;font-size:13px;margin-bottom:10px;}.checkbox label:before{content:"";display:inline-block;width:18px;height:18px;margin-right:10px;position:absolute;left:0;bottom:1px;border:2px solid #eee;border-radius:3px;}.checkbox input[type=checkbox]{display:none;}.checkbox input[type=checkbox]:checked+label:before{content:"✓";font-size:13px;color:#fafafa;text-align:center;line-height:13px;}.input-group-sm{padding-top:2px;padding-bottom:2px;}.tab-pane .form-horizontal .form-group{margin-right:15px;margin-left:15px;}.bootstrap-datetimepicker-widget.dropdown-menu{background-color:#4e5d6c;}.bootstrap-datetimepicker-widget.dropdown-menu.bottom:after{border-bottom:6px solid #4e5d6c !important;}.bootstrap-datetimepicker-widget table td.active,.bootstrap-datetimepicker-widget table td.active:hover{color:#fff !important;}.landing-header{display:block;margin:60px auto;}.landing-block{background:#2f2f2f !important;padding:5px;}.landing-block .media{margin:30px auto;max-width:450px;}.landing-block .media .media-left{display:inline-block;float:left;width:70px;}.landing-block .media .media-left i.fa{font-size:3em;}.landing-title{font-weight:bold;}.checkbox-custom{margin-top:0 !important;margin-bottom:0 !important;}.tooltip_templates{display:none;}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/base.scss b/PlexRequests.UI/Content/base.scss
index 8b5a1e45e..9bb6b156b 100644
--- a/PlexRequests.UI/Content/base.scss
+++ b/PlexRequests.UI/Content/base.scss
@@ -419,4 +419,5 @@ $border-radius: 10px;
.checkbox-custom{
margin-top:0 $i;
margin-bottom:0 $i;
-}
\ No newline at end of file
+}
+.tooltip_templates { display: none; }
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/requests.js b/PlexRequests.UI/Content/requests.js
index f9e19e505..cbd9136fd 100644
--- a/PlexRequests.UI/Content/requests.js
+++ b/PlexRequests.UI/Content/requests.js
@@ -574,10 +574,29 @@ function tvLoad() {
var url = createBaseUrl(base, '/requests/tvshows');
$.ajax(url).success(function (results) {
if (results.length > 0) {
+ var tvObject = new Array();
results.forEach(function (result) {
+ var ep = result.episodes;
+ ep.forEach(function (episode, index) {
+ if (!tvObject.find(x => x.seasonNumber === episode.seasonNumber)) {
+ var obj = { seasonNumber: episode.seasonNumber, episodes: [] }
+ tvObject.push(obj);
+ tvObject[index].episodes.push(episode.episodeNumber);
+ } else {
+ var selectedObj =tvObject.find(x => x.seasonNumber === episode.seasonNumber);
+ selectedObj.episodes.push(episode.episodeNumber);
+ }
+ });
+
var context = buildRequestContext(result, "tv");
+ context.episodes = tvObject;
var html = searchTemplate(context);
$tvl.append(html);
+
+ });
+
+ $('.customTooltip').tooltipster({
+ contentCloning: true
});
}
else {
@@ -638,7 +657,6 @@ function buildRequestContext(result, type) {
hasQualities: result.qualities && result.qualities.length > 0,
artist: result.artistName,
musicBrainzId: result.musicBrainzId,
- episodes : result.episodes
};
return context;
diff --git a/PlexRequests.UI/Content/site.js b/PlexRequests.UI/Content/site.js
index 7978839c1..ff04f6a97 100644
--- a/PlexRequests.UI/Content/site.js
+++ b/PlexRequests.UI/Content/site.js
@@ -8,6 +8,10 @@
return s;
}
+$(function() {
+ $('[data-toggle="tooltip"]').tooltip();
+});
+
var mainBaseUrl = $('#baseUrl').text();
function Humanize(date) {
diff --git a/PlexRequests.UI/Content/tooltip/plugins/tooltipster/SVG/tooltipster-SVG.js b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/SVG/tooltipster-SVG.js
new file mode 100644
index 000000000..8d94dc7a7
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/SVG/tooltipster-SVG.js
@@ -0,0 +1,171 @@
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define(["tooltipster"], function (a0) {
+ return (factory(a0));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory(require("tooltipster"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define(["jquery"], function (a0) {
+ return (factory(a0));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+
+var pluginName = 'tooltipster.SVG';
+
+$.tooltipster._plugin({
+ name: pluginName,
+ core: {
+ __init: function() {
+
+ $.tooltipster._on('init', function(event) {
+
+ var win = $.tooltipster._env.window;
+
+ if ( win.SVGElement
+ && event.origin instanceof win.SVGElement
+ ) {
+
+ // auto-activation of the plugin on the instance
+ event.instance._plug(pluginName);
+ }
+ });
+ }
+ },
+ instance: {
+ __init: function(instance) {
+
+ var self = this;
+
+ //list of instance variables
+ self.__hadTitleTag = false;
+ self.__instance = instance;
+
+ // jQuery < v3.0's addClass and hasClass do not work on SVG elements.
+ // However, $('.tooltipstered') does find elements having the class.
+ if (!self.__instance._$origin.hasClass('tooltipstered')) {
+
+ var c = self.__instance._$origin.attr('class') || '';
+
+ if (c.indexOf('tooltipstered') == -1) {
+ self.__instance._$origin.attr('class', c + ' tooltipstered');
+ }
+ }
+
+ // if there is no content yet, let's look for a
child element
+ if (self.__instance.content() === null) {
+
+ // TODO: when there are several tags (not supported in
+ // today's browsers yet though, still an RFC draft), pick the right
+ // one based on its "lang" attribute
+ var $title = self.__instance._$origin.find('>title');
+
+ if ($title[0]) {
+
+ var title = $title.text();
+
+ self.__hadTitleTag = true;
+ self.__instance._$origin.data('tooltipster-initialTitle', title);
+ self.__instance.content(title);
+
+ $title.remove();
+ }
+ }
+
+ // rectify the geometry if SVG.js and its screenBBox plugin have been included
+ self.__instance
+ ._on('geometry.'+ self.namespace, function(event) {
+
+ var win = $.tooltipster._env.window;
+
+ // SVG coordinates may need fixing but we need svg.screenbox.js
+ // to provide it. SVGElement is IE8+
+ if (win.SVG.svgjs) {
+
+ if (!win.SVG.parser) {
+ win.SVG.prepare();
+ }
+
+ var svgEl = win.SVG.adopt(event.origin);
+
+ // not all figures need (and have) screenBBox
+ if (svgEl && svgEl.screenBBox) {
+
+ var bbox = svgEl.screenBBox();
+
+ event.edit({
+ height: bbox.height,
+ left: bbox.x,
+ top: bbox.y,
+ width: bbox.width
+ });
+ }
+ }
+ })
+ // if jQuery < v3.0, we have to remove the class ourselves
+ ._on('destroy.'+ self.namespace, function() {
+ self.__destroy();
+ });
+ },
+
+ __destroy: function() {
+
+ var self = this;
+
+ if (!self.__instance._$origin.hasClass('tooltipstered')) {
+ var c = self.__instance._$origin.attr('class').replace('tooltipstered', '');
+ self.__instance._$origin.attr('class', c);
+ }
+
+ self.__instance._off('.'+ self.namespace);
+
+ // if the content was provided as a title tag, we may need to restore it
+ if (self.__hadTitleTag) {
+
+ // this must happen after Tooltipster restored (or not) the title attr
+ self.__instance.one('destroyed', function() {
+
+ // if a title attribute was restored, we just need to replace it with a tag
+ var title = self.__instance._$origin.attr('title');
+
+ if (title) {
+
+ // must be namespaced to work
+ $(document.createElementNS('http://www.w3.org/2000/svg', 'title'))
+ .text(title)
+ .appendTo(self.__instance._$origin);
+
+ self.__instance._$origin.removeAttr('title');
+ }
+ });
+ }
+ }
+ }
+});
+
+/* a build task will add "return $;" here */
+return $;
+
+}));
+
+
+}));
diff --git a/PlexRequests.UI/Content/tooltip/plugins/tooltipster/SVG/tooltipster-SVG.min.js b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/SVG/tooltipster-SVG.min.js
new file mode 100644
index 000000000..df7706877
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/SVG/tooltipster-SVG.min.js
@@ -0,0 +1 @@
+!function(a,b){"function"==typeof define&&define.amd?define(["tooltipster"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("tooltipster")):b(jQuery)}(this,function(a){!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){var b="tooltipster.SVG";return a.tooltipster._plugin({name:b,core:{__init:function(){a.tooltipster._on("init",function(c){var d=a.tooltipster._env.window;d.SVGElement&&c.origin instanceof d.SVGElement&&c.instance._plug(b)})}},instance:{__init:function(b){var c=this;if(c.__hadTitleTag=!1,c.__instance=b,!c.__instance._$origin.hasClass("tooltipstered")){var d=c.__instance._$origin.attr("class")||"";-1==d.indexOf("tooltipstered")&&c.__instance._$origin.attr("class",d+" tooltipstered")}if(null===c.__instance.content()){var e=c.__instance._$origin.find(">title");if(e[0]){var f=e.text();c.__hadTitleTag=!0,c.__instance._$origin.data("tooltipster-initialTitle",f),c.__instance.content(f),e.remove()}}c.__instance._on("geometry."+c.namespace,function(b){var c=a.tooltipster._env.window;if(c.SVG.svgjs){c.SVG.parser||c.SVG.prepare();var d=c.SVG.adopt(b.origin);if(d&&d.screenBBox){var e=d.screenBBox();b.edit({height:e.height,left:e.x,top:e.y,width:e.width})}}})._on("destroy."+c.namespace,function(){c.__destroy()})},__destroy:function(){var b=this;if(!b.__instance._$origin.hasClass("tooltipstered")){var c=b.__instance._$origin.attr("class").replace("tooltipstered","");b.__instance._$origin.attr("class",c)}b.__instance._off("."+b.namespace),b.__hadTitleTag&&b.__instance.one("destroyed",function(){var c=b.__instance._$origin.attr("title");c&&(a(document.createElementNS("http://www.w3.org/2000/svg","title")).text(c).appendTo(b.__instance._$origin),b.__instance._$origin.removeAttr("title"))})}}}),a})});
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-borderless.min.css b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-borderless.min.css
new file mode 100644
index 000000000..19408cb1e
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-borderless.min.css
@@ -0,0 +1 @@
+.tooltipster-sidetip.tooltipster-borderless .tooltipster-box{border:none;background:#1b1b1b;background:rgba(10,10,10,.9)}.tooltipster-sidetip.tooltipster-borderless.tooltipster-bottom .tooltipster-box{margin-top:8px}.tooltipster-sidetip.tooltipster-borderless.tooltipster-left .tooltipster-box{margin-right:8px}.tooltipster-sidetip.tooltipster-borderless.tooltipster-right .tooltipster-box{margin-left:8px}.tooltipster-sidetip.tooltipster-borderless.tooltipster-top .tooltipster-box{margin-bottom:8px}.tooltipster-sidetip.tooltipster-borderless .tooltipster-arrow{height:8px;margin-left:-8px;width:16px}.tooltipster-sidetip.tooltipster-borderless.tooltipster-left .tooltipster-arrow,.tooltipster-sidetip.tooltipster-borderless.tooltipster-right .tooltipster-arrow{height:16px;margin-left:0;margin-top:-8px;width:8px}.tooltipster-sidetip.tooltipster-borderless .tooltipster-arrow-background{display:none}.tooltipster-sidetip.tooltipster-borderless .tooltipster-arrow-border{border:8px solid transparent}.tooltipster-sidetip.tooltipster-borderless.tooltipster-bottom .tooltipster-arrow-border{border-bottom-color:#1b1b1b;border-bottom-color:rgba(10,10,10,.9)}.tooltipster-sidetip.tooltipster-borderless.tooltipster-left .tooltipster-arrow-border{border-left-color:#1b1b1b;border-left-color:rgba(10,10,10,.9)}.tooltipster-sidetip.tooltipster-borderless.tooltipster-right .tooltipster-arrow-border{border-right-color:#1b1b1b;border-right-color:rgba(10,10,10,.9)}.tooltipster-sidetip.tooltipster-borderless.tooltipster-top .tooltipster-arrow-border{border-top-color:#1b1b1b;border-top-color:rgba(10,10,10,.9)}.tooltipster-sidetip.tooltipster-borderless.tooltipster-bottom .tooltipster-arrow-uncropped{top:-8px}.tooltipster-sidetip.tooltipster-borderless.tooltipster-right .tooltipster-arrow-uncropped{left:-8px}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-light.min.css b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-light.min.css
new file mode 100644
index 000000000..298c9d4a5
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-light.min.css
@@ -0,0 +1 @@
+.tooltipster-sidetip.tooltipster-light .tooltipster-box{border-radius:3px;border:1px solid #ccc;background:#ededed}.tooltipster-sidetip.tooltipster-light .tooltipster-content{color:#666}.tooltipster-sidetip.tooltipster-light .tooltipster-arrow{height:9px;margin-left:-9px;width:18px}.tooltipster-sidetip.tooltipster-light.tooltipster-left .tooltipster-arrow,.tooltipster-sidetip.tooltipster-light.tooltipster-right .tooltipster-arrow{height:18px;margin-left:0;margin-top:-9px;width:9px}.tooltipster-sidetip.tooltipster-light .tooltipster-arrow-background{border:9px solid transparent}.tooltipster-sidetip.tooltipster-light.tooltipster-bottom .tooltipster-arrow-background{border-bottom-color:#ededed;top:1px}.tooltipster-sidetip.tooltipster-light.tooltipster-left .tooltipster-arrow-background{border-left-color:#ededed;left:-1px}.tooltipster-sidetip.tooltipster-light.tooltipster-right .tooltipster-arrow-background{border-right-color:#ededed;left:1px}.tooltipster-sidetip.tooltipster-light.tooltipster-top .tooltipster-arrow-background{border-top-color:#ededed;top:-1px}.tooltipster-sidetip.tooltipster-light .tooltipster-arrow-border{border:9px solid transparent}.tooltipster-sidetip.tooltipster-light.tooltipster-bottom .tooltipster-arrow-border{border-bottom-color:#ccc}.tooltipster-sidetip.tooltipster-light.tooltipster-left .tooltipster-arrow-border{border-left-color:#ccc}.tooltipster-sidetip.tooltipster-light.tooltipster-right .tooltipster-arrow-border{border-right-color:#ccc}.tooltipster-sidetip.tooltipster-light.tooltipster-top .tooltipster-arrow-border{border-top-color:#ccc}.tooltipster-sidetip.tooltipster-light.tooltipster-bottom .tooltipster-arrow-uncropped{top:-9px}.tooltipster-sidetip.tooltipster-light.tooltipster-right .tooltipster-arrow-uncropped{left:-9px}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-noir.min.css b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-noir.min.css
new file mode 100644
index 000000000..39f4ca388
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-noir.min.css
@@ -0,0 +1 @@
+.tooltipster-sidetip.tooltipster-noir .tooltipster-box{border-radius:0;border:3px solid #000;background:#fff}.tooltipster-sidetip.tooltipster-noir .tooltipster-content{color:#000}.tooltipster-sidetip.tooltipster-noir .tooltipster-arrow{height:11px;margin-left:-11px;width:22px}.tooltipster-sidetip.tooltipster-noir.tooltipster-left .tooltipster-arrow,.tooltipster-sidetip.tooltipster-noir.tooltipster-right .tooltipster-arrow{height:22px;margin-left:0;margin-top:-11px;width:11px}.tooltipster-sidetip.tooltipster-noir .tooltipster-arrow-background{border:11px solid transparent}.tooltipster-sidetip.tooltipster-noir.tooltipster-bottom .tooltipster-arrow-background{border-bottom-color:#fff;top:4px}.tooltipster-sidetip.tooltipster-noir.tooltipster-left .tooltipster-arrow-background{border-left-color:#fff;left:-4px}.tooltipster-sidetip.tooltipster-noir.tooltipster-right .tooltipster-arrow-background{border-right-color:#fff;left:4px}.tooltipster-sidetip.tooltipster-noir.tooltipster-top .tooltipster-arrow-background{border-top-color:#fff;top:-4px}.tooltipster-sidetip.tooltipster-noir .tooltipster-arrow-border{border-width:11px}.tooltipster-sidetip.tooltipster-noir.tooltipster-bottom .tooltipster-arrow-uncropped{top:-11px}.tooltipster-sidetip.tooltipster-noir.tooltipster-right .tooltipster-arrow-uncropped{left:-11px}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-punk.min.css b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-punk.min.css
new file mode 100644
index 000000000..6702cf557
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-punk.min.css
@@ -0,0 +1 @@
+.tooltipster-sidetip.tooltipster-punk .tooltipster-box{border-radius:5px;border:none;border-bottom:3px solid #f71169;background:#2a2a2a}.tooltipster-sidetip.tooltipster-punk.tooltipster-top .tooltipster-box{margin-bottom:7px}.tooltipster-sidetip.tooltipster-punk .tooltipster-content{color:#fff;padding:8px 16px}.tooltipster-sidetip.tooltipster-punk .tooltipster-arrow-background{display:none}.tooltipster-sidetip.tooltipster-punk.tooltipster-bottom .tooltipster-arrow-border{border-bottom-color:#2a2a2a}.tooltipster-sidetip.tooltipster-punk.tooltipster-left .tooltipster-arrow-border{border-left-color:#2a2a2a}.tooltipster-sidetip.tooltipster-punk.tooltipster-right .tooltipster-arrow-border{border-right-color:#2a2a2a}.tooltipster-sidetip.tooltipster-punk.tooltipster-top .tooltipster-arrow-border{border-top-color:#f71169}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css
new file mode 100644
index 000000000..7d92926de
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/themes/tooltipster-sideTip-shadow.min.css
@@ -0,0 +1 @@
+.tooltipster-sidetip.tooltipster-shadow .tooltipster-box{border:none;border-radius:5px;background:#fff;box-shadow:0 0 10px 6px rgba(0,0,0,.1)}.tooltipster-sidetip.tooltipster-shadow.tooltipster-bottom .tooltipster-box{margin-top:6px}.tooltipster-sidetip.tooltipster-shadow.tooltipster-left .tooltipster-box{margin-right:6px}.tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-box{margin-left:6px}.tooltipster-sidetip.tooltipster-shadow.tooltipster-top .tooltipster-box{margin-bottom:6px}.tooltipster-sidetip.tooltipster-shadow .tooltipster-content{color:#8d8d8d}.tooltipster-sidetip.tooltipster-shadow .tooltipster-arrow{height:6px;margin-left:-6px;width:12px}.tooltipster-sidetip.tooltipster-shadow.tooltipster-left .tooltipster-arrow,.tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-arrow{height:12px;margin-left:0;margin-top:-6px;width:6px}.tooltipster-sidetip.tooltipster-shadow .tooltipster-arrow-background{display:none}.tooltipster-sidetip.tooltipster-shadow .tooltipster-arrow-border{border:6px solid transparent}.tooltipster-sidetip.tooltipster-shadow.tooltipster-bottom .tooltipster-arrow-border{border-bottom-color:#fff}.tooltipster-sidetip.tooltipster-shadow.tooltipster-left .tooltipster-arrow-border{border-left-color:#fff}.tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-arrow-border{border-right-color:#fff}.tooltipster-sidetip.tooltipster-shadow.tooltipster-top .tooltipster-arrow-border{border-top-color:#fff}.tooltipster-sidetip.tooltipster-shadow.tooltipster-bottom .tooltipster-arrow-uncropped{top:-6px}.tooltipster-sidetip.tooltipster-shadow.tooltipster-right .tooltipster-arrow-uncropped{left:-6px}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/tooltipster-sideTip.min.css b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/tooltipster-sideTip.min.css
new file mode 100644
index 000000000..911139aab
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/plugins/tooltipster/sideTip/tooltipster-sideTip.min.css
@@ -0,0 +1 @@
+.tooltipster-sidetip .tooltipster-box{background:#565656;border:2px solid #000;border-radius:4px}.tooltipster-sidetip.tooltipster-bottom .tooltipster-box{margin-top:8px}.tooltipster-sidetip.tooltipster-left .tooltipster-box{margin-right:8px}.tooltipster-sidetip.tooltipster-right .tooltipster-box{margin-left:8px}.tooltipster-sidetip.tooltipster-top .tooltipster-box{margin-bottom:8px}.tooltipster-sidetip .tooltipster-content{color:#fff;line-height:18px;padding:6px 14px}.tooltipster-sidetip .tooltipster-arrow{overflow:hidden;position:absolute}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow{height:10px;margin-left:-10px;top:0;width:20px}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow{height:20px;margin-top:-10px;right:0;top:0;width:10px}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow{height:20px;margin-top:-10px;left:0;top:0;width:10px}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow{bottom:0;height:10px;margin-left:-10px;width:20px}.tooltipster-sidetip .tooltipster-arrow-background,.tooltipster-sidetip .tooltipster-arrow-border{height:0;position:absolute;width:0}.tooltipster-sidetip .tooltipster-arrow-background{border:10px solid transparent}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-background{border-bottom-color:#565656;left:0;top:3px}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow-background{border-left-color:#565656;left:-3px;top:0}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-background{border-right-color:#565656;left:3px;top:0}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow-background{border-top-color:#565656;left:0;top:-3px}.tooltipster-sidetip .tooltipster-arrow-border{border:10px solid transparent;left:0;top:0}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-border{border-bottom-color:#000}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow-border{border-left-color:#000}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-border{border-right-color:#000}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow-border{border-top-color:#000}.tooltipster-sidetip .tooltipster-arrow-uncropped{position:relative}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-uncropped{top:-10px}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-uncropped{left:-10px}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/tooltip/tooltipster.bundle.css b/PlexRequests.UI/Content/tooltip/tooltipster.bundle.css
new file mode 100644
index 000000000..9e140476b
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/tooltipster.bundle.css
@@ -0,0 +1,388 @@
+/* This is the core CSS of Tooltipster */
+
+/* GENERAL STRUCTURE RULES (do not edit this section) */
+
+.tooltipster-base {
+ /* this ensures that a constrained height set by functionPosition,
+ if greater that the natural height of the tooltip, will be enforced
+ in browsers that support display:flex */
+ display: flex;
+ pointer-events: none;
+ /* this may be overriden in JS for fixed position origins */
+ position: absolute;
+}
+
+.tooltipster-box {
+ /* see .tooltipster-base. flex-shrink 1 is only necessary for IE10-
+ and flex-basis auto for IE11- (at least) */
+ flex: 1 1 auto;
+}
+
+.tooltipster-content {
+ /* prevents an overflow if the user adds padding to the div */
+ box-sizing: border-box;
+ /* these make sure we'll be able to detect any overflow */
+ max-height: 100%;
+ max-width: 100%;
+ overflow: auto;
+}
+
+.tooltipster-ruler {
+ /* these let us test the size of the tooltip without overflowing the window */
+ bottom: 0;
+ left: 0;
+ overflow: hidden;
+ position: fixed;
+ right: 0;
+ top: 0;
+ visibility: hidden;
+}
+
+/* ANIMATIONS */
+
+/* Open/close animations */
+
+/* fade */
+
+.tooltipster-fade {
+ opacity: 0;
+ -webkit-transition-property: opacity;
+ -moz-transition-property: opacity;
+ -o-transition-property: opacity;
+ -ms-transition-property: opacity;
+ transition-property: opacity;
+}
+.tooltipster-fade.tooltipster-show {
+ opacity: 1;
+}
+
+/* grow */
+
+.tooltipster-grow {
+ -webkit-transform: scale(0,0);
+ -moz-transform: scale(0,0);
+ -o-transform: scale(0,0);
+ -ms-transform: scale(0,0);
+ transform: scale(0,0);
+ -webkit-transition-property: -webkit-transform;
+ -moz-transition-property: -moz-transform;
+ -o-transition-property: -o-transform;
+ -ms-transition-property: -ms-transform;
+ transition-property: transform;
+ -webkit-backface-visibility: hidden;
+}
+.tooltipster-grow.tooltipster-show {
+ -webkit-transform: scale(1,1);
+ -moz-transform: scale(1,1);
+ -o-transform: scale(1,1);
+ -ms-transform: scale(1,1);
+ transform: scale(1,1);
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+}
+
+/* swing */
+
+.tooltipster-swing {
+ opacity: 0;
+ -webkit-transform: rotateZ(4deg);
+ -moz-transform: rotateZ(4deg);
+ -o-transform: rotateZ(4deg);
+ -ms-transform: rotateZ(4deg);
+ transform: rotateZ(4deg);
+ -webkit-transition-property: -webkit-transform, opacity;
+ -moz-transition-property: -moz-transform;
+ -o-transition-property: -o-transform;
+ -ms-transition-property: -ms-transform;
+ transition-property: transform;
+}
+.tooltipster-swing.tooltipster-show {
+ opacity: 1;
+ -webkit-transform: rotateZ(0deg);
+ -moz-transform: rotateZ(0deg);
+ -o-transform: rotateZ(0deg);
+ -ms-transform: rotateZ(0deg);
+ transform: rotateZ(0deg);
+ -webkit-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 1);
+ -webkit-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
+ -moz-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
+ -ms-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
+ -o-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
+ transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
+}
+
+/* fall */
+
+.tooltipster-fall {
+ -webkit-transition-property: top;
+ -moz-transition-property: top;
+ -o-transition-property: top;
+ -ms-transition-property: top;
+ transition-property: top;
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+}
+.tooltipster-fall.tooltipster-initial {
+ top: 0 !important;
+}
+.tooltipster-fall.tooltipster-show {
+}
+.tooltipster-fall.tooltipster-dying {
+ -webkit-transition-property: all;
+ -moz-transition-property: all;
+ -o-transition-property: all;
+ -ms-transition-property: all;
+ transition-property: all;
+ top: 0 !important;
+ opacity: 0;
+}
+
+/* slide */
+
+.tooltipster-slide {
+ -webkit-transition-property: left;
+ -moz-transition-property: left;
+ -o-transition-property: left;
+ -ms-transition-property: left;
+ transition-property: left;
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+}
+.tooltipster-slide.tooltipster-initial {
+ left: -40px !important;
+}
+.tooltipster-slide.tooltipster-show {
+}
+.tooltipster-slide.tooltipster-dying {
+ -webkit-transition-property: all;
+ -moz-transition-property: all;
+ -o-transition-property: all;
+ -ms-transition-property: all;
+ transition-property: all;
+ left: 0 !important;
+ opacity: 0;
+}
+
+/* Update animations */
+
+/* We use animations rather than transitions here because
+ transition durations may be specified in the style tag due to
+ animationDuration, and we try to avoid collisions and the use
+ of !important */
+
+/* fade */
+
+@keyframes tooltipster-fading {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+.tooltipster-update-fade {
+ animation: tooltipster-fading 400ms;
+}
+
+/* rotate */
+
+@keyframes tooltipster-rotating {
+ 25% {
+ transform: rotate(-2deg);
+ }
+ 75% {
+ transform: rotate(2deg);
+ }
+ 100% {
+ transform: rotate(0);
+ }
+}
+
+.tooltipster-update-rotate {
+ animation: tooltipster-rotating 600ms;
+}
+
+/* scale */
+
+@keyframes tooltipster-scaling {
+ 50% {
+ transform: scale(1.1);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+.tooltipster-update-scale {
+ animation: tooltipster-scaling 600ms;
+}
+
+/**
+ * DEFAULT STYLE OF THE SIDETIP PLUGIN
+ *
+ * All styles are "namespaced" with .tooltipster-sidetip to prevent
+ * conflicts between plugins.
+ */
+
+/* .tooltipster-box */
+
+.tooltipster-sidetip .tooltipster-box {
+ background: #565656;
+ border: 2px solid black;
+ border-radius: 4px;
+}
+
+.tooltipster-sidetip.tooltipster-bottom .tooltipster-box {
+ margin-top: 8px;
+}
+
+.tooltipster-sidetip.tooltipster-left .tooltipster-box {
+ margin-right: 8px;
+}
+
+.tooltipster-sidetip.tooltipster-right .tooltipster-box {
+ margin-left: 8px;
+}
+
+.tooltipster-sidetip.tooltipster-top .tooltipster-box {
+ margin-bottom: 8px;
+}
+
+/* .tooltipster-content */
+
+.tooltipster-sidetip .tooltipster-content {
+ color: white;
+ line-height: 18px;
+ padding: 6px 14px;
+}
+
+/* .tooltipster-arrow : will keep only the zone of .tooltipster-arrow-uncropped that
+corresponds to the arrow we want to display */
+
+.tooltipster-sidetip .tooltipster-arrow {
+ overflow: hidden;
+ position: absolute;
+}
+
+.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow {
+ height: 10px;
+ /* half the width, for centering */
+ margin-left: -10px;
+ top: 0;
+ width: 20px;
+}
+
+.tooltipster-sidetip.tooltipster-left .tooltipster-arrow {
+ height: 20px;
+ margin-top: -10px;
+ right: 0;
+ /* top 0 to keep the arrow from overflowing .tooltipster-base when it has not
+ been positioned yet */
+ top: 0;
+ width: 10px;
+}
+
+.tooltipster-sidetip.tooltipster-right .tooltipster-arrow {
+ height: 20px;
+ margin-top: -10px;
+ left: 0;
+ /* same as .tooltipster-left .tooltipster-arrow */
+ top: 0;
+ width: 10px;
+}
+
+.tooltipster-sidetip.tooltipster-top .tooltipster-arrow {
+ bottom: 0;
+ height: 10px;
+ margin-left: -10px;
+ width: 20px;
+}
+
+/* common rules between .tooltipster-arrow-background and .tooltipster-arrow-border */
+
+.tooltipster-sidetip .tooltipster-arrow-background, .tooltipster-sidetip .tooltipster-arrow-border {
+ height: 0;
+ position: absolute;
+ width: 0;
+}
+
+/* .tooltipster-arrow-background */
+
+.tooltipster-sidetip .tooltipster-arrow-background {
+ border: 10px solid transparent;
+}
+
+.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-background {
+ border-bottom-color: #565656;
+ left: 0px;
+ top: 3px;
+}
+
+.tooltipster-sidetip.tooltipster-left .tooltipster-arrow-background {
+ border-left-color: #565656;
+ left: -3px;
+ top: 0px;
+}
+
+.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-background {
+ border-right-color: #565656;
+ left: 3px;
+ top: 0px;
+}
+
+.tooltipster-sidetip.tooltipster-top .tooltipster-arrow-background {
+ border-top-color: #565656;
+ left: 0px;
+ top: -3px;
+}
+
+/* .tooltipster-arrow-border */
+
+.tooltipster-sidetip .tooltipster-arrow-border {
+ border: 10px solid transparent;
+ left: 0;
+ top: 0;
+}
+
+.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-border {
+ border-bottom-color: black;
+}
+
+.tooltipster-sidetip.tooltipster-left .tooltipster-arrow-border {
+ border-left-color: black;
+}
+
+.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-border {
+ border-right-color: black;
+}
+
+.tooltipster-sidetip.tooltipster-top .tooltipster-arrow-border {
+ border-top-color: black;
+}
+
+/* tooltipster-arrow-uncropped */
+
+.tooltipster-sidetip .tooltipster-arrow-uncropped {
+ position: relative;
+}
+
+.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-uncropped {
+ top: -10px;
+}
+
+.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-uncropped {
+ left: -10px;
+}
diff --git a/PlexRequests.UI/Content/tooltip/tooltipster.bundle.js b/PlexRequests.UI/Content/tooltip/tooltipster.bundle.js
new file mode 100644
index 000000000..e10307650
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/tooltipster.bundle.js
@@ -0,0 +1,4223 @@
+/**
+ * tooltipster http://iamceege.github.io/tooltipster/
+ * A rockin' custom tooltip jQuery plugin
+ * Developed by Caleb Jacob and Louis Ameline
+ * MIT license
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define(["jquery"], function (a0) {
+ return (factory(a0));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+
+// This file will be UMDified by a build task.
+
+var defaults = {
+ animation: 'fade',
+ animationDuration: 350,
+ content: null,
+ contentAsHTML: false,
+ contentCloning: false,
+ debug: true,
+ delay: 300,
+ delayTouch: [300, 500],
+ functionInit: null,
+ functionBefore: null,
+ functionReady: null,
+ functionAfter: null,
+ functionFormat: null,
+ IEmin: 6,
+ interactive: false,
+ multiple: false,
+ // must be 'body' for now, or an element positioned at (0, 0)
+ // in the document, typically like the very top views of an app.
+ parent: 'body',
+ plugins: ['sideTip'],
+ repositionOnScroll: false,
+ restoration: 'none',
+ selfDestruction: true,
+ theme: [],
+ timer: 0,
+ trackerInterval: 500,
+ trackOrigin: false,
+ trackTooltip: false,
+ trigger: 'hover',
+ triggerClose: {
+ click: false,
+ mouseleave: false,
+ originClick: false,
+ scroll: false,
+ tap: false,
+ touchleave: false
+ },
+ triggerOpen: {
+ click: false,
+ mouseenter: false,
+ tap: false,
+ touchstart: false
+ },
+ updateAnimation: 'rotate',
+ zIndex: 9999999
+ },
+ // we'll avoid using the 'window' global as a good practice but npm's
+ // jquery@<2.1.0 package actually requires a 'window' global, so not sure
+ // it's useful at all
+ win = (typeof window != 'undefined') ? window : null,
+ // env will be proxied by the core for plugins to have access its properties
+ env = {
+ // detect if this device can trigger touch events. Better have a false
+ // positive (unused listeners, that's ok) than a false negative.
+ // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js
+ // http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
+ hasTouchCapability: !!(
+ win
+ && ( 'ontouchstart' in win
+ || (win.DocumentTouch && win.document instanceof win.DocumentTouch)
+ || win.navigator.maxTouchPoints
+ )
+ ),
+ hasTransitions: transitionSupport(),
+ IE: false,
+ // don't set manually, it will be updated by a build task after the manifest
+ semVer: '4.1.2',
+ window: win
+ },
+ core = function() {
+
+ // core variables
+
+ // the core emitters
+ this.__$emitterPrivate = $({});
+ this.__$emitterPublic = $({});
+ this.__instancesLatestArr = [];
+ // collects plugin constructors
+ this.__plugins = {};
+ // proxy env variables for plugins who might use them
+ this._env = env;
+ };
+
+// core methods
+core.prototype = {
+
+ /**
+ * A function to proxy the public methods of an object onto another
+ *
+ * @param {object} constructor The constructor to bridge
+ * @param {object} obj The object that will get new methods (an instance or the core)
+ * @param {string} pluginName A plugin name for the console log message
+ * @return {core}
+ * @private
+ */
+ __bridge: function(constructor, obj, pluginName) {
+
+ // if it's not already bridged
+ if (!obj[pluginName]) {
+
+ var fn = function() {};
+ fn.prototype = constructor;
+
+ var pluginInstance = new fn();
+
+ // the _init method has to exist in instance constructors but might be missing
+ // in core constructors
+ if (pluginInstance.__init) {
+ pluginInstance.__init(obj);
+ }
+
+ $.each(constructor, function(methodName, fn) {
+
+ // don't proxy "private" methods, only "protected" and public ones
+ if (methodName.indexOf('__') != 0) {
+
+ // if the method does not exist yet
+ if (!obj[methodName]) {
+
+ obj[methodName] = function() {
+ return pluginInstance[methodName].apply(pluginInstance, Array.prototype.slice.apply(arguments));
+ };
+
+ // remember to which plugin this method corresponds (several plugins may
+ // have methods of the same name, we need to be sure)
+ obj[methodName].bridged = pluginInstance;
+ }
+ else if (defaults.debug) {
+
+ console.log('The '+ methodName +' method of the '+ pluginName
+ +' plugin conflicts with another plugin or native methods');
+ }
+ }
+ });
+
+ obj[pluginName] = pluginInstance;
+ }
+
+ return this;
+ },
+
+ /**
+ * For mockup in Node env if need be, for testing purposes
+ *
+ * @return {core}
+ * @private
+ */
+ __setWindow: function(window) {
+ env.window = window;
+ return this;
+ },
+
+ /**
+ * Returns a ruler, a tool to help measure the size of a tooltip under
+ * various settings. Meant for plugins
+ *
+ * @see Ruler
+ * @return {object} A Ruler instance
+ * @protected
+ */
+ _getRuler: function($tooltip) {
+ return new Ruler($tooltip);
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @return {core}
+ * @protected
+ */
+ _off: function() {
+ this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @return {core}
+ * @protected
+ */
+ _on: function() {
+ this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @return {core}
+ * @protected
+ */
+ _one: function() {
+ this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * Returns (getter) or adds (setter) a plugin
+ *
+ * @param {string|object} plugin Provide a string (in the full form
+ * "namespace.name") to use as as getter, an object to use as a setter
+ * @return {object|core}
+ * @protected
+ */
+ _plugin: function(plugin) {
+
+ var self = this;
+
+ // getter
+ if (typeof plugin == 'string') {
+
+ var pluginName = plugin,
+ p = null;
+
+ // if the namespace is provided, it's easy to search
+ if (pluginName.indexOf('.') > 0) {
+ p = self.__plugins[pluginName];
+ }
+ // otherwise, return the first name that matches
+ else {
+ $.each(self.__plugins, function(i, plugin) {
+
+ if (plugin.name.substring(plugin.name.length - pluginName.length - 1) == '.'+ pluginName) {
+ p = plugin;
+ return false;
+ }
+ });
+ }
+
+ return p;
+ }
+ // setter
+ else {
+
+ // force namespaces
+ if (plugin.name.indexOf('.') < 0) {
+ throw new Error('Plugins must be namespaced');
+ }
+
+ self.__plugins[plugin.name] = plugin;
+
+ // if the plugin has core features
+ if (plugin.core) {
+
+ // bridge non-private methods onto the core to allow new core methods
+ self.__bridge(plugin.core, self, plugin.name);
+ }
+
+ return this;
+ }
+ },
+
+ /**
+ * Trigger events on the core emitters
+ *
+ * @returns {core}
+ * @protected
+ */
+ _trigger: function() {
+
+ var args = Array.prototype.slice.apply(arguments);
+
+ if (typeof args[0] == 'string') {
+ args[0] = { type: args[0] };
+ }
+
+ // note: the order of emitters matters
+ this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
+ this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);
+
+ return this;
+ },
+
+ /**
+ * Returns instances of all tooltips in the page or an a given element
+ *
+ * @param {string|HTML object collection} selector optional Use this
+ * parameter to restrict the set of objects that will be inspected
+ * for the retrieval of instances. By default, all instances in the
+ * page are returned.
+ * @return {array} An array of instance objects
+ * @public
+ */
+ instances: function(selector) {
+
+ var instances = [],
+ sel = selector || '.tooltipstered';
+
+ $(sel).each(function() {
+
+ var $this = $(this),
+ ns = $this.data('tooltipster-ns');
+
+ if (ns) {
+
+ $.each(ns, function(i, namespace) {
+ instances.push($this.data(namespace));
+ });
+ }
+ });
+
+ return instances;
+ },
+
+ /**
+ * Returns the Tooltipster objects generated by the last initializing call
+ *
+ * @return {array} An array of instance objects
+ * @public
+ */
+ instancesLatest: function() {
+ return this.__instancesLatestArr;
+ },
+
+ /**
+ * For public use only, not to be used by plugins (use ::_off() instead)
+ *
+ * @return {core}
+ * @public
+ */
+ off: function() {
+ this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For public use only, not to be used by plugins (use ::_on() instead)
+ *
+ * @return {core}
+ * @public
+ */
+ on: function() {
+ this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For public use only, not to be used by plugins (use ::_one() instead)
+ *
+ * @return {core}
+ * @public
+ */
+ one: function() {
+ this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * Returns all HTML elements which have one or more tooltips
+ *
+ * @param {string} selector optional Use this to restrict the results
+ * to the descendants of an element
+ * @return {array} An array of HTML elements
+ * @public
+ */
+ origins: function(selector) {
+
+ var sel = selector ?
+ selector +' ' :
+ '';
+
+ return $(sel +'.tooltipstered').toArray();
+ },
+
+ /**
+ * Change default options for all future instances
+ *
+ * @param {object} d The options that should be made defaults
+ * @return {core}
+ * @public
+ */
+ setDefaults: function(d) {
+ $.extend(defaults, d);
+ return this;
+ },
+
+ /**
+ * For users to trigger their handlers on the public emitter
+ *
+ * @returns {core}
+ * @public
+ */
+ triggerHandler: function() {
+ this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ return this;
+ }
+};
+
+// $.tooltipster will be used to call core methods
+$.tooltipster = new core();
+
+// the Tooltipster instance class (mind the capital T)
+$.Tooltipster = function(element, options) {
+
+ // list of instance variables
+
+ // stack of custom callbacks provided as parameters to API methods
+ this.__callbacks = {
+ close: [],
+ open: []
+ };
+ // the schedule time of DOM removal
+ this.__closingTime;
+ // this will be the user content shown in the tooltip. A capital "C" is used
+ // because there is also a method called content()
+ this.__Content;
+ // for the size tracker
+ this.__contentBcr;
+ // to disable the tooltip once the destruction has begun
+ this.__destroyed = false;
+ this.__destroying = false;
+ // we can't emit directly on the instance because if a method with the same
+ // name as the event exists, it will be called by jQuery. Se we use a plain
+ // object as emitter. This emitter is for internal use by plugins,
+ // if needed.
+ this.__$emitterPrivate = $({});
+ // this emitter is for the user to listen to events without risking to mess
+ // with our internal listeners
+ this.__$emitterPublic = $({});
+ this.__enabled = true;
+ // the reference to the gc interval
+ this.__garbageCollector;
+ // various position and size data recomputed before each repositioning
+ this.__Geometry;
+ // the tooltip position, saved after each repositioning by a plugin
+ this.__lastPosition;
+ // a unique namespace per instance
+ this.__namespace = 'tooltipster-'+ Math.round(Math.random()*1000000);
+ this.__options;
+ // will be used to support origins in scrollable areas
+ this.__$originParents;
+ this.__pointerIsOverOrigin = false;
+ // to remove themes if needed
+ this.__previousThemes = [];
+ // the state can be either: appearing, stable, disappearing, closed
+ this.__state = 'closed';
+ // timeout references
+ this.__timeouts = {
+ close: [],
+ open: null
+ };
+ // store touch events to be able to detect emulated mouse events
+ this.__touchEvents = [];
+ // the reference to the tracker interval
+ this.__tracker = null;
+ // the element to which this tooltip is associated
+ this._$origin;
+ // this will be the tooltip element (jQuery wrapped HTML element).
+ // It's the job of a plugin to create it and append it to the DOM
+ this._$tooltip;
+
+ // launch
+ this.__init(element, options);
+};
+
+$.Tooltipster.prototype = {
+
+ /**
+ * @param origin
+ * @param options
+ * @private
+ */
+ __init: function(origin, options) {
+
+ var self = this;
+
+ self._$origin = $(origin);
+ self.__options = $.extend(true, {}, defaults, options);
+
+ // some options may need to be reformatted
+ self.__optionsFormat();
+
+ // don't run on old IE if asked no to
+ if ( !env.IE
+ || env.IE >= self.__options.IEmin
+ ) {
+
+ // note: the content is null (empty) by default and can stay that
+ // way if the plugin remains initialized but not fed any content. The
+ // tooltip will just not appear.
+
+ // let's save the initial value of the title attribute for later
+ // restoration if need be.
+ var initialTitle = null;
+
+ // it will already have been saved in case of multiple tooltips
+ if (self._$origin.data('tooltipster-initialTitle') === undefined) {
+
+ initialTitle = self._$origin.attr('title');
+
+ // we do not want initialTitle to be "undefined" because
+ // of how jQuery's .data() method works
+ if (initialTitle === undefined) initialTitle = null;
+
+ self._$origin.data('tooltipster-initialTitle', initialTitle);
+ }
+
+ // If content is provided in the options, it has precedence over the
+ // title attribute.
+ // Note: an empty string is considered content, only 'null' represents
+ // the absence of content.
+ // Also, an existing title="" attribute will result in an empty string
+ // content
+ if (self.__options.content !== null) {
+ self.__contentSet(self.__options.content);
+ }
+ else {
+
+ var selector = self._$origin.attr('data-tooltip-content'),
+ $el;
+
+ if (selector){
+ $el = $(selector);
+ }
+
+ if ($el && $el[0]) {
+ self.__contentSet($el.first());
+ }
+ else {
+ self.__contentSet(initialTitle);
+ }
+ }
+
+ self._$origin
+ // strip the title off of the element to prevent the default tooltips
+ // from popping up
+ .removeAttr('title')
+ // to be able to find all instances on the page later (upon window
+ // events in particular)
+ .addClass('tooltipstered');
+
+ // set listeners on the origin
+ self.__prepareOrigin();
+
+ // set the garbage collector
+ self.__prepareGC();
+
+ // init plugins
+ $.each(self.__options.plugins, function(i, pluginName) {
+ self._plug(pluginName);
+ });
+
+ // to detect swiping
+ if (env.hasTouchCapability) {
+ $('body').on('touchmove.'+ self.__namespace +'-triggerOpen', function(event) {
+ self._touchRecordEvent(event);
+ });
+ }
+
+ self
+ // prepare the tooltip when it gets created. This event must
+ // be fired by a plugin
+ ._on('created', function() {
+ self.__prepareTooltip();
+ })
+ // save position information when it's sent by a plugin
+ ._on('repositioned', function(e) {
+ self.__lastPosition = e.position;
+ });
+ }
+ else {
+ self.__options.disabled = true;
+ }
+ },
+
+ /**
+ * Insert the content into the appropriate HTML element of the tooltip
+ *
+ * @returns {self}
+ * @private
+ */
+ __contentInsert: function() {
+
+ var self = this,
+ $el = self._$tooltip.find('.tooltipster-content'),
+ formattedContent = self.__Content,
+ format = function(content) {
+ formattedContent = content;
+ };
+
+ self._trigger({
+ type: 'format',
+ content: self.__Content,
+ format: format
+ });
+
+ if (self.__options.functionFormat) {
+
+ formattedContent = self.__options.functionFormat.call(
+ self,
+ self,
+ { origin: self._$origin[0] },
+ self.__Content
+ );
+ }
+
+ if (typeof formattedContent === 'string' && !self.__options.contentAsHTML) {
+ $el.text(formattedContent);
+ }
+ else {
+ $el
+ .empty()
+ .append(formattedContent);
+ }
+
+ return self;
+ },
+
+ /**
+ * Save the content, cloning it beforehand if need be
+ *
+ * @param content
+ * @returns {self}
+ * @private
+ */
+ __contentSet: function(content) {
+
+ // clone if asked. Cloning the object makes sure that each instance has its
+ // own version of the content (in case a same object were provided for several
+ // instances)
+ // reminder: typeof null === object
+ if (content instanceof $ && this.__options.contentCloning) {
+ content = content.clone(true);
+ }
+
+ this.__Content = content;
+
+ this._trigger({
+ type: 'updated',
+ content: content
+ });
+
+ return this;
+ },
+
+ /**
+ * Error message about a method call made after destruction
+ *
+ * @private
+ */
+ __destroyError: function() {
+ throw new Error('This tooltip has been destroyed and cannot execute your method call.');
+ },
+
+ /**
+ * Gather all information about dimensions and available space,
+ * called before every repositioning
+ *
+ * @private
+ * @returns {object}
+ */
+ __geometry: function() {
+
+ var self = this,
+ $target = self._$origin,
+ originIsArea = self._$origin.is('area');
+
+ // if this._$origin is a map area, the target we'll need
+ // the dimensions of is actually the image using the map,
+ // not the area itself
+ if (originIsArea) {
+
+ var mapName = self._$origin.parent().attr('name');
+
+ $target = $('img[usemap="#'+ mapName +'"]');
+ }
+
+ var bcr = $target[0].getBoundingClientRect(),
+ $document = $(env.window.document),
+ $window = $(env.window),
+ $parent = $target,
+ // some useful properties of important elements
+ geo = {
+ // available space for the tooltip, see down below
+ available: {
+ document: null,
+ window: null
+ },
+ document: {
+ size: {
+ height: $document.height(),
+ width: $document.width()
+ }
+ },
+ window: {
+ scroll: {
+ // the second ones are for IE compatibility
+ left: env.window.scrollX || env.window.document.documentElement.scrollLeft,
+ top: env.window.scrollY || env.window.document.documentElement.scrollTop
+ },
+ size: {
+ height: $window.height(),
+ width: $window.width()
+ }
+ },
+ origin: {
+ // the origin has a fixed lineage if itself or one of its
+ // ancestors has a fixed position
+ fixedLineage: false,
+ // relative to the document
+ offset: {},
+ size: {
+ height: bcr.bottom - bcr.top,
+ width: bcr.right - bcr.left
+ },
+ usemapImage: originIsArea ? $target[0] : null,
+ // relative to the window
+ windowOffset: {
+ bottom: bcr.bottom,
+ left: bcr.left,
+ right: bcr.right,
+ top: bcr.top
+ }
+ }
+ },
+ geoFixed = false;
+
+ // if the element is a map area, some properties may need
+ // to be recalculated
+ if (originIsArea) {
+
+ var shape = self._$origin.attr('shape'),
+ coords = self._$origin.attr('coords');
+
+ if (coords) {
+
+ coords = coords.split(',');
+
+ $.map(coords, function(val, i) {
+ coords[i] = parseInt(val);
+ });
+ }
+
+ // if the image itself is the area, nothing more to do
+ if (shape != 'default') {
+
+ switch(shape) {
+
+ case 'circle':
+
+ var circleCenterLeft = coords[0],
+ circleCenterTop = coords[1],
+ circleRadius = coords[2],
+ areaTopOffset = circleCenterTop - circleRadius,
+ areaLeftOffset = circleCenterLeft - circleRadius;
+
+ geo.origin.size.height = circleRadius * 2;
+ geo.origin.size.width = geo.origin.size.height;
+
+ geo.origin.windowOffset.left += areaLeftOffset;
+ geo.origin.windowOffset.top += areaTopOffset;
+
+ break;
+
+ case 'rect':
+
+ var areaLeft = coords[0],
+ areaTop = coords[1],
+ areaRight = coords[2],
+ areaBottom = coords[3];
+
+ geo.origin.size.height = areaBottom - areaTop;
+ geo.origin.size.width = areaRight - areaLeft;
+
+ geo.origin.windowOffset.left += areaLeft;
+ geo.origin.windowOffset.top += areaTop;
+
+ break;
+
+ case 'poly':
+
+ var areaSmallestX = 0,
+ areaSmallestY = 0,
+ areaGreatestX = 0,
+ areaGreatestY = 0,
+ arrayAlternate = 'even';
+
+ for (var i = 0; i < coords.length; i++) {
+
+ var areaNumber = coords[i];
+
+ if (arrayAlternate == 'even') {
+
+ if (areaNumber > areaGreatestX) {
+
+ areaGreatestX = areaNumber;
+
+ if (i === 0) {
+ areaSmallestX = areaGreatestX;
+ }
+ }
+
+ if (areaNumber < areaSmallestX) {
+ areaSmallestX = areaNumber;
+ }
+
+ arrayAlternate = 'odd';
+ }
+ else {
+ if (areaNumber > areaGreatestY) {
+
+ areaGreatestY = areaNumber;
+
+ if (i == 1) {
+ areaSmallestY = areaGreatestY;
+ }
+ }
+
+ if (areaNumber < areaSmallestY) {
+ areaSmallestY = areaNumber;
+ }
+
+ arrayAlternate = 'even';
+ }
+ }
+
+ geo.origin.size.height = areaGreatestY - areaSmallestY;
+ geo.origin.size.width = areaGreatestX - areaSmallestX;
+
+ geo.origin.windowOffset.left += areaSmallestX;
+ geo.origin.windowOffset.top += areaSmallestY;
+
+ break;
+ }
+ }
+ }
+
+ // user callback through an event
+ var edit = function(r) {
+ geo.origin.size.height = r.height,
+ geo.origin.windowOffset.left = r.left,
+ geo.origin.windowOffset.top = r.top,
+ geo.origin.size.width = r.width
+ };
+
+ self._trigger({
+ type: 'geometry',
+ edit: edit,
+ geometry: {
+ height: geo.origin.size.height,
+ left: geo.origin.windowOffset.left,
+ top: geo.origin.windowOffset.top,
+ width: geo.origin.size.width
+ }
+ });
+
+ // calculate the remaining properties with what we got
+
+ geo.origin.windowOffset.right = geo.origin.windowOffset.left + geo.origin.size.width;
+ geo.origin.windowOffset.bottom = geo.origin.windowOffset.top + geo.origin.size.height;
+
+ geo.origin.offset.left = geo.origin.windowOffset.left + env.window.scrollX;
+ geo.origin.offset.top = geo.origin.windowOffset.top + env.window.scrollY;
+ geo.origin.offset.bottom = geo.origin.offset.top + geo.origin.size.height;
+ geo.origin.offset.right = geo.origin.offset.left + geo.origin.size.width;
+
+ // the space that is available to display the tooltip relatively to the document
+ geo.available.document = {
+ bottom: {
+ height: geo.document.size.height - geo.origin.offset.bottom,
+ width: geo.document.size.width
+ },
+ left: {
+ height: geo.document.size.height,
+ width: geo.origin.offset.left
+ },
+ right: {
+ height: geo.document.size.height,
+ width: geo.document.size.width - geo.origin.offset.right
+ },
+ top: {
+ height: geo.origin.offset.top,
+ width: geo.document.size.width
+ }
+ };
+
+ // the space that is available to display the tooltip relatively to the viewport
+ // (the resulting values may be negative if the origin overflows the viewport)
+ geo.available.window = {
+ bottom: {
+ // the inner max is here to make sure the available height is no bigger
+ // than the viewport height (when the origin is off screen at the top).
+ // The outer max just makes sure that the height is not negative (when
+ // the origin overflows at the bottom).
+ height: Math.max(geo.window.size.height - Math.max(geo.origin.windowOffset.bottom, 0), 0),
+ width: geo.window.size.width
+ },
+ left: {
+ height: geo.window.size.height,
+ width: Math.max(geo.origin.windowOffset.left, 0)
+ },
+ right: {
+ height: geo.window.size.height,
+ width: Math.max(geo.window.size.width - Math.max(geo.origin.windowOffset.right, 0), 0)
+ },
+ top: {
+ height: Math.max(geo.origin.windowOffset.top, 0),
+ width: geo.window.size.width
+ }
+ };
+
+ while ($parent[0].tagName.toLowerCase() != 'html') {
+
+ if ($parent.css('position') == 'fixed') {
+ geo.origin.fixedLineage = true;
+ break;
+ }
+
+ $parent = $parent.parent();
+ }
+
+ return geo;
+ },
+
+ /**
+ * Some options may need to be formated before being used
+ *
+ * @returns {self}
+ * @private
+ */
+ __optionsFormat: function() {
+
+ if (typeof this.__options.animationDuration == 'number') {
+ this.__options.animationDuration = [this.__options.animationDuration, this.__options.animationDuration];
+ }
+
+ if (typeof this.__options.delay == 'number') {
+ this.__options.delay = [this.__options.delay, this.__options.delay];
+ }
+
+ if (typeof this.__options.delayTouch == 'number') {
+ this.__options.delayTouch = [this.__options.delayTouch, this.__options.delayTouch];
+ }
+
+ if (typeof this.__options.theme == 'string') {
+ this.__options.theme = [this.__options.theme];
+ }
+
+ // determine the future parent
+ if (typeof this.__options.parent == 'string') {
+ this.__options.parent = $(this.__options.parent);
+ }
+
+ if (this.__options.trigger == 'hover') {
+
+ this.__options.triggerOpen = {
+ mouseenter: true,
+ touchstart: true
+ };
+
+ this.__options.triggerClose = {
+ mouseleave: true,
+ originClick: true,
+ touchleave: true
+ };
+ }
+ else if (this.__options.trigger == 'click') {
+
+ this.__options.triggerOpen = {
+ click: true,
+ tap: true
+ };
+
+ this.__options.triggerClose = {
+ click: true,
+ tap: true
+ };
+ }
+
+ // for the plugins
+ this._trigger('options');
+
+ return this;
+ },
+
+ /**
+ * Schedules or cancels the garbage collector task
+ *
+ * @returns {self}
+ * @private
+ */
+ __prepareGC: function() {
+
+ var self = this;
+
+ // in case the selfDestruction option has been changed by a method call
+ if (self.__options.selfDestruction) {
+
+ // the GC task
+ self.__garbageCollector = setInterval(function() {
+
+ var now = new Date().getTime();
+
+ // forget the old events
+ self.__touchEvents = $.grep(self.__touchEvents, function(event, i) {
+ // 1 minute
+ return now - event.time > 60000;
+ });
+
+ // auto-destruct if the origin is gone
+ if (!bodyContains(self._$origin)) {
+ self.destroy();
+ }
+ }, 20000);
+ }
+ else {
+ clearInterval(self.__garbageCollector);
+ }
+
+ return self;
+ },
+
+ /**
+ * Sets listeners on the origin if the open triggers require them.
+ * Unlike the listeners set at opening time, these ones
+ * remain even when the tooltip is closed. It has been made a
+ * separate method so it can be called when the triggers are
+ * changed in the options. Closing is handled in _open()
+ * because of the bindings that may be needed on the tooltip
+ * itself
+ *
+ * @returns {self}
+ * @private
+ */
+ __prepareOrigin: function() {
+
+ var self = this;
+
+ // in case we're resetting the triggers
+ self._$origin.off('.'+ self.__namespace +'-triggerOpen');
+
+ // if the device is touch capable, even if only mouse triggers
+ // are asked, we need to listen to touch events to know if the mouse
+ // events are actually emulated (so we can ignore them)
+ if (env.hasTouchCapability) {
+
+ self._$origin.on(
+ 'touchstart.'+ self.__namespace +'-triggerOpen ' +
+ 'touchend.'+ self.__namespace +'-triggerOpen ' +
+ 'touchcancel.'+ self.__namespace +'-triggerOpen',
+ function(event){
+ self._touchRecordEvent(event);
+ }
+ );
+ }
+
+ // mouse click and touch tap work the same way
+ if ( self.__options.triggerOpen.click
+ || (self.__options.triggerOpen.tap && env.hasTouchCapability)
+ ) {
+
+ var eventNames = '';
+ if (self.__options.triggerOpen.click) {
+ eventNames += 'click.'+ self.__namespace +'-triggerOpen ';
+ }
+ if (self.__options.triggerOpen.tap && env.hasTouchCapability) {
+ eventNames += 'touchend.'+ self.__namespace +'-triggerOpen';
+ }
+
+ self._$origin.on(eventNames, function(event) {
+ if (self._touchIsMeaningfulEvent(event)) {
+ self._open(event);
+ }
+ });
+ }
+
+ // mouseenter and touch start work the same way
+ if ( self.__options.triggerOpen.mouseenter
+ || (self.__options.triggerOpen.touchstart && env.hasTouchCapability)
+ ) {
+
+ var eventNames = '';
+ if (self.__options.triggerOpen.mouseenter) {
+ eventNames += 'mouseenter.'+ self.__namespace +'-triggerOpen ';
+ }
+ if (self.__options.triggerOpen.touchstart && env.hasTouchCapability) {
+ eventNames += 'touchstart.'+ self.__namespace +'-triggerOpen';
+ }
+
+ self._$origin.on(eventNames, function(event) {
+ if ( self._touchIsTouchEvent(event)
+ || !self._touchIsEmulatedEvent(event)
+ ) {
+ self.__pointerIsOverOrigin = true;
+ self._openShortly(event);
+ }
+ });
+ }
+
+ // info for the mouseleave/touchleave close triggers when they use a delay
+ if ( self.__options.triggerClose.mouseleave
+ || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
+ ) {
+
+ var eventNames = '';
+ if (self.__options.triggerClose.mouseleave) {
+ eventNames += 'mouseleave.'+ self.__namespace +'-triggerOpen ';
+ }
+ if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
+ eventNames += 'touchend.'+ self.__namespace +'-triggerOpen touchcancel.'+ self.__namespace +'-triggerOpen';
+ }
+
+ self._$origin.on(eventNames, function(event) {
+
+ if (self._touchIsMeaningfulEvent(event)) {
+ self.__pointerIsOverOrigin = false;
+ }
+ });
+ }
+
+ return self;
+ },
+
+ /**
+ * Do the things that need to be done only once after the tooltip
+ * HTML element it has been created. It has been made a separate
+ * method so it can be called when options are changed. Remember
+ * that the tooltip may actually exist in the DOM before it is
+ * opened, and present after it has been closed: it's the display
+ * plugin that takes care of handling it.
+ *
+ * @returns {self}
+ * @private
+ */
+ __prepareTooltip: function() {
+
+ var self = this,
+ p = self.__options.interactive ? 'auto' : '';
+
+ // this will be useful to know quickly if the tooltip is in
+ // the DOM or not
+ self._$tooltip
+ .attr('id', self.__namespace)
+ .css({
+ // pointer events
+ 'pointer-events': p,
+ zIndex: self.__options.zIndex
+ });
+
+ // themes
+ // remove the old ones and add the new ones
+ $.each(self.__previousThemes, function(i, theme) {
+ self._$tooltip.removeClass(theme);
+ });
+ $.each(self.__options.theme, function(i, theme) {
+ self._$tooltip.addClass(theme);
+ });
+
+ self.__previousThemes = $.merge([], self.__options.theme);
+
+ return self;
+ },
+
+ /**
+ * Handles the scroll on any of the parents of the origin (when the
+ * tooltip is open)
+ *
+ * @param {object} event
+ * @returns {self}
+ * @private
+ */
+ __scrollHandler: function(event) {
+
+ var self = this;
+
+ if (self.__options.triggerClose.scroll) {
+ self._close(event);
+ }
+ else {
+
+ // if the scroll happened on the window
+ if (event.target === env.window.document) {
+
+ // if the origin has a fixed lineage, window scroll will have no
+ // effect on its position nor on the position of the tooltip
+ if (!self.__Geometry.origin.fixedLineage) {
+
+ // we don't need to do anything unless repositionOnScroll is true
+ // because the tooltip will already have moved with the window
+ // (and of course with the origin)
+ if (self.__options.repositionOnScroll) {
+ self.reposition(event);
+ }
+ }
+ }
+ // if the scroll happened on another parent of the tooltip, it means
+ // that it's in a scrollable area and now needs to have its position
+ // adjusted or recomputed, depending ont the repositionOnScroll
+ // option. Also, if the origin is partly hidden due to a parent that
+ // hides its overflow, we'll just hide (not close) the tooltip.
+ else {
+
+ var g = self.__geometry(),
+ overflows = false;
+
+ // a fixed position origin is not affected by the overflow hiding
+ // of a parent
+ if (self._$origin.css('position') != 'fixed') {
+
+ self.__$originParents.each(function(i, el) {
+
+ var $el = $(el),
+ overflowX = $el.css('overflow-x'),
+ overflowY = $el.css('overflow-y');
+
+ if (overflowX != 'visible' || overflowY != 'visible') {
+
+ var bcr = el.getBoundingClientRect();
+
+ if (overflowX != 'visible') {
+
+ if ( g.origin.windowOffset.left < bcr.left
+ || g.origin.windowOffset.right > bcr.right
+ ) {
+ overflows = true;
+ return false;
+ }
+ }
+
+ if (overflowY != 'visible') {
+
+ if ( g.origin.windowOffset.top < bcr.top
+ || g.origin.windowOffset.bottom > bcr.bottom
+ ) {
+ overflows = true;
+ return false;
+ }
+ }
+ }
+
+ // no need to go further if fixed, for the same reason as above
+ if ($el.css('position') == 'fixed') {
+ return false;
+ }
+ });
+ }
+
+ if (overflows) {
+ self._$tooltip.css('visibility', 'hidden');
+ }
+ else {
+ self._$tooltip.css('visibility', 'visible');
+
+ // reposition
+ if (self.__options.repositionOnScroll) {
+ self.reposition(event);
+ }
+ // or just adjust offset
+ else {
+
+ // we have to use offset and not windowOffset because this way,
+ // only the scroll distance of the scrollable areas are taken into
+ // account (the scrolltop value of the main window must be
+ // ignored since the tooltip already moves with it)
+ var offsetLeft = g.origin.offset.left - self.__Geometry.origin.offset.left,
+ offsetTop = g.origin.offset.top - self.__Geometry.origin.offset.top;
+
+ // add the offset to the position initially computed by the display plugin
+ self._$tooltip.css({
+ left: self.__lastPosition.coord.left + offsetLeft,
+ top: self.__lastPosition.coord.top + offsetTop
+ });
+ }
+ }
+ }
+
+ self._trigger({
+ type: 'scroll',
+ event: event
+ });
+ }
+
+ return self;
+ },
+
+ /**
+ * Changes the state of the tooltip
+ *
+ * @param {string} state
+ * @returns {self}
+ * @private
+ */
+ __stateSet: function(state) {
+
+ this.__state = state;
+
+ this._trigger({
+ type: 'state',
+ state: state
+ });
+
+ return this;
+ },
+
+ /**
+ * Clear appearance timeouts
+ *
+ * @returns {self}
+ * @private
+ */
+ __timeoutsClear: function() {
+
+ // there is only one possible open timeout: the delayed opening
+ // when the mouseenter/touchstart open triggers are used
+ clearTimeout(this.__timeouts.open);
+ this.__timeouts.open = null;
+
+ // ... but several close timeouts: the delayed closing when the
+ // mouseleave close trigger is used and the timer option
+ $.each(this.__timeouts.close, function(i, timeout) {
+ clearTimeout(timeout);
+ });
+ this.__timeouts.close = [];
+
+ return this;
+ },
+
+ /**
+ * Start the tracker that will make checks at regular intervals
+ *
+ * @returns {self}
+ * @private
+ */
+ __trackerStart: function() {
+
+ var self = this,
+ $content = self._$tooltip.find('.tooltipster-content');
+
+ // get the initial content size
+ if (self.__options.trackTooltip) {
+ self.__contentBcr = $content[0].getBoundingClientRect();
+ }
+
+ self.__tracker = setInterval(function() {
+
+ // if the origin or tooltip elements have been removed.
+ // Note: we could destroy the instance now if the origin has
+ // been removed but we'll leave that task to our garbage collector
+ if (!bodyContains(self._$origin) || !bodyContains(self._$tooltip)) {
+ self._close();
+ }
+ // if everything is alright
+ else {
+
+ // compare the former and current positions of the origin to reposition
+ // the tooltip if need be
+ if (self.__options.trackOrigin) {
+
+ var g = self.__geometry(),
+ identical = false;
+
+ // compare size first (a change requires repositioning too)
+ if (areEqual(g.origin.size, self.__Geometry.origin.size)) {
+
+ // for elements that have a fixed lineage (see __geometry()), we track the
+ // top and left properties (relative to window)
+ if (self.__Geometry.origin.fixedLineage) {
+ if (areEqual(g.origin.windowOffset, self.__Geometry.origin.windowOffset)) {
+ identical = true;
+ }
+ }
+ // otherwise, track total offset (relative to document)
+ else {
+ if (areEqual(g.origin.offset, self.__Geometry.origin.offset)) {
+ identical = true;
+ }
+ }
+ }
+
+ if (!identical) {
+
+ // close the tooltip when using the mouseleave close trigger
+ // (see https://github.com/iamceege/tooltipster/pull/253)
+ if (self.__options.triggerClose.mouseleave) {
+ self._close();
+ }
+ else {
+ self.reposition();
+ }
+ }
+ }
+
+ if (self.__options.trackTooltip) {
+
+ var currentBcr = $content[0].getBoundingClientRect();
+
+ if ( currentBcr.height !== self.__contentBcr.height
+ || currentBcr.width !== self.__contentBcr.width
+ ) {
+ self.reposition();
+ self.__contentBcr = currentBcr;
+ }
+ }
+ }
+ }, self.__options.trackerInterval);
+
+ return self;
+ },
+
+ /**
+ * Closes the tooltip (after the closing delay)
+ *
+ * @param event
+ * @param callback
+ * @returns {self}
+ * @protected
+ */
+ _close: function(event, callback) {
+
+ var self = this,
+ ok = true;
+
+ self._trigger({
+ type: 'close',
+ event: event,
+ stop: function() {
+ ok = false;
+ }
+ });
+
+ // a destroying tooltip may not refuse to close
+ if (ok || self.__destroying) {
+
+ // save the method custom callback and cancel any open method custom callbacks
+ if (callback) self.__callbacks.close.push(callback);
+ self.__callbacks.open = [];
+
+ // clear open/close timeouts
+ self.__timeoutsClear();
+
+ var finishCallbacks = function() {
+
+ // trigger any close method custom callbacks and reset them
+ $.each(self.__callbacks.close, function(i,c) {
+ c.call(self, self, {
+ event: event,
+ origin: self._$origin[0]
+ });
+ });
+
+ self.__callbacks.close = [];
+ };
+
+ if (self.__state != 'closed') {
+
+ var necessary = true,
+ d = new Date(),
+ now = d.getTime(),
+ newClosingTime = now + self.__options.animationDuration[1];
+
+ // the tooltip may already already be disappearing, but if a new
+ // call to close() is made after the animationDuration was changed
+ // to 0 (for example), we ought to actually close it sooner than
+ // previously scheduled. In that case it should be noted that the
+ // browser will not adapt the animation duration to the new
+ // animationDuration that was set after the start of the closing
+ // animation.
+ // Note: the same thing could be considered at opening, but is not
+ // really useful since the tooltip is actually opened immediately
+ // upon a call to _open(). Since it would not make the opening
+ // animation finish sooner, its sole impact would be to trigger the
+ // state event and the open callbacks sooner than the actual end of
+ // the opening animation, which is not great.
+ if (self.__state == 'disappearing') {
+
+ if (newClosingTime > self.__closingTime) {
+ necessary = false;
+ }
+ }
+
+ if (necessary) {
+
+ self.__closingTime = newClosingTime;
+
+ if (self.__state != 'disappearing') {
+ self.__stateSet('disappearing');
+ }
+
+ var finish = function() {
+
+ // stop the tracker
+ clearInterval(self.__tracker);
+
+ // a "beforeClose" option has been asked several times but would
+ // probably useless since the content element is still accessible
+ // via ::content(), and because people can always use listeners
+ // inside their content to track what's going on. For the sake of
+ // simplicity, this has been denied. Bur for the rare people who
+ // really need the option (for old browsers or for the case where
+ // detaching the content is actually destructive, for file or
+ // password inputs for example), this event will do the work.
+ self._trigger({
+ type: 'closing',
+ event: event
+ });
+
+ // unbind listeners which are no longer needed
+
+ self._$tooltip
+ .off('.'+ self.__namespace +'-triggerClose')
+ .removeClass('tooltipster-dying');
+
+ // orientationchange, scroll and resize listeners
+ $(env.window).off('.'+ self.__namespace +'-triggerClose');
+
+ // scroll listeners
+ self.__$originParents.each(function(i, el) {
+ $(el).off('scroll.'+ self.__namespace +'-triggerClose');
+ });
+ // clear the array to prevent memory leaks
+ self.__$originParents = null;
+
+ $('body').off('.'+ self.__namespace +'-triggerClose');
+
+ self._$origin.off('.'+ self.__namespace +'-triggerClose');
+
+ self._off('dismissable');
+
+ // a plugin that would like to remove the tooltip from the
+ // DOM when closed should bind on this
+ self.__stateSet('closed');
+
+ // trigger event
+ self._trigger({
+ type: 'after',
+ event: event
+ });
+
+ // call our constructor custom callback function
+ if (self.__options.functionAfter) {
+ self.__options.functionAfter.call(self, self, {
+ event: event
+ });
+ }
+
+ // call our method custom callbacks functions
+ finishCallbacks();
+ };
+
+ if (env.hasTransitions) {
+
+ self._$tooltip.css({
+ '-moz-animation-duration': self.__options.animationDuration[1] + 'ms',
+ '-ms-animation-duration': self.__options.animationDuration[1] + 'ms',
+ '-o-animation-duration': self.__options.animationDuration[1] + 'ms',
+ '-webkit-animation-duration': self.__options.animationDuration[1] + 'ms',
+ 'animation-duration': self.__options.animationDuration[1] + 'ms',
+ 'transition-duration': self.__options.animationDuration[1] + 'ms'
+ });
+
+ self._$tooltip
+ // clear both potential open and close tasks
+ .clearQueue()
+ .removeClass('tooltipster-show')
+ // for transitions only
+ .addClass('tooltipster-dying');
+
+ if (self.__options.animationDuration[1] > 0) {
+ self._$tooltip.delay(self.__options.animationDuration[1]);
+ }
+
+ self._$tooltip.queue(finish);
+ }
+ else {
+
+ self._$tooltip
+ .stop()
+ .fadeOut(self.__options.animationDuration[1], finish);
+ }
+ }
+ }
+ // if the tooltip is already closed, we still need to trigger
+ // the method custom callbacks
+ else {
+ finishCallbacks();
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @returns {self}
+ * @protected
+ */
+ _off: function() {
+ this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @returns {self}
+ * @protected
+ */
+ _on: function() {
+ this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @returns {self}
+ * @protected
+ */
+ _one: function() {
+ this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * Opens the tooltip right away
+ *
+ * @param event
+ * @param callback
+ * @returns {self}
+ * @protected
+ */
+ _open: function(event, callback) {
+
+ var self = this;
+
+ // if the destruction process has not begun and if this was not
+ // triggered by an unwanted emulated click event
+ if (!self.__destroying) {
+
+ // check that the origin is still in the DOM
+ if ( bodyContains(self._$origin)
+ // if the tooltip is enabled
+ && self.__enabled
+ ) {
+
+ var ok = true;
+
+ // if the tooltip is not open yet, we need to call functionBefore.
+ // otherwise we can jst go on
+ if (self.__state == 'closed') {
+
+ // trigger an event. The event.stop function allows the callback
+ // to prevent the opening of the tooltip
+ self._trigger({
+ type: 'before',
+ event: event,
+ stop: function() {
+ ok = false;
+ }
+ });
+
+ if (ok && self.__options.functionBefore) {
+
+ // call our custom function before continuing
+ ok = self.__options.functionBefore.call(self, self, {
+ event: event,
+ origin: self._$origin[0]
+ });
+ }
+ }
+
+ if (ok !== false) {
+
+ // if there is some content
+ if (self.__Content !== null) {
+
+ // save the method callback and cancel close method callbacks
+ if (callback) {
+ self.__callbacks.open.push(callback);
+ }
+ self.__callbacks.close = [];
+
+ // get rid of any appearance timeouts
+ self.__timeoutsClear();
+
+ var extraTime,
+ finish = function() {
+
+ if (self.__state != 'stable') {
+ self.__stateSet('stable');
+ }
+
+ // trigger any open method custom callbacks and reset them
+ $.each(self.__callbacks.open, function(i,c) {
+ c.call(self, self, {
+ origin: self._$origin[0],
+ tooltip: self._$tooltip[0]
+ });
+ });
+
+ self.__callbacks.open = [];
+ };
+
+ // if the tooltip is already open
+ if (self.__state !== 'closed') {
+
+ // the timer (if any) will start (or restart) right now
+ extraTime = 0;
+
+ // if it was disappearing, cancel that
+ if (self.__state === 'disappearing') {
+
+ self.__stateSet('appearing');
+
+ if (env.hasTransitions) {
+
+ self._$tooltip
+ .clearQueue()
+ .removeClass('tooltipster-dying')
+ .addClass('tooltipster-show');
+
+ if (self.__options.animationDuration[0] > 0) {
+ self._$tooltip.delay(self.__options.animationDuration[0]);
+ }
+
+ self._$tooltip.queue(finish);
+ }
+ else {
+ // in case the tooltip was currently fading out, bring it back
+ // to life
+ self._$tooltip
+ .stop()
+ .fadeIn(finish);
+ }
+ }
+ // if the tooltip is already open, we still need to trigger the method
+ // custom callback
+ else if (self.__state == 'stable') {
+ finish();
+ }
+ }
+ // if the tooltip isn't already open, open it
+ else {
+
+ // a plugin must bind on this and store the tooltip in this._$tooltip
+ self.__stateSet('appearing');
+
+ // the timer (if any) will start when the tooltip has fully appeared
+ // after its transition
+ extraTime = self.__options.animationDuration[0];
+
+ // insert the content inside the tooltip
+ self.__contentInsert();
+
+ // reposition the tooltip and attach to the DOM
+ self.reposition(event, true);
+
+ // animate in the tooltip. If the display plugin wants no css
+ // animations, it may override the animation option with a
+ // dummy value that will produce no effect
+ if (env.hasTransitions) {
+
+ // note: there seems to be an issue with start animations which
+ // are randomly not played on fast devices in both Chrome and FF,
+ // couldn't find a way to solve it yet. It seems that applying
+ // the classes before appending to the DOM helps a little, but
+ // it messes up some CSS transitions. The issue almost never
+ // happens when delay[0]==0 though
+ self._$tooltip
+ .addClass('tooltipster-'+ self.__options.animation)
+ .addClass('tooltipster-initial')
+ .css({
+ '-moz-animation-duration': self.__options.animationDuration[0] + 'ms',
+ '-ms-animation-duration': self.__options.animationDuration[0] + 'ms',
+ '-o-animation-duration': self.__options.animationDuration[0] + 'ms',
+ '-webkit-animation-duration': self.__options.animationDuration[0] + 'ms',
+ 'animation-duration': self.__options.animationDuration[0] + 'ms',
+ 'transition-duration': self.__options.animationDuration[0] + 'ms'
+ });
+
+ setTimeout(
+ function() {
+
+ // a quick hover may have already triggered a mouseleave
+ if (self.__state != 'closed') {
+
+ self._$tooltip
+ .addClass('tooltipster-show')
+ .removeClass('tooltipster-initial');
+
+ if (self.__options.animationDuration[0] > 0) {
+ self._$tooltip.delay(self.__options.animationDuration[0]);
+ }
+
+ self._$tooltip.queue(finish);
+ }
+ },
+ 0
+ );
+ }
+ else {
+
+ // old browsers will have to live with this
+ self._$tooltip
+ .css('display', 'none')
+ .fadeIn(self.__options.animationDuration[0], finish);
+ }
+
+ // checks if the origin is removed while the tooltip is open
+ self.__trackerStart();
+
+ // NOTE: the listeners below have a '-triggerClose' namespace
+ // because we'll remove them when the tooltip closes (unlike
+ // the '-triggerOpen' listeners). So some of them are actually
+ // not about close triggers, rather about positioning.
+
+ $(env.window)
+ // reposition on resize
+ .on('resize.'+ self.__namespace +'-triggerClose', function(e) {
+ self.reposition(e);
+ })
+ // same as below for parents
+ .on('scroll.'+ self.__namespace +'-triggerClose', function(e) {
+ self.__scrollHandler(e);
+ });
+
+ self.__$originParents = self._$origin.parents();
+
+ // scrolling may require the tooltip to be moved or even
+ // repositioned in some cases
+ self.__$originParents.each(function(i, parent) {
+
+ $(parent).on('scroll.'+ self.__namespace +'-triggerClose', function(e) {
+ self.__scrollHandler(e);
+ });
+ });
+
+ if ( self.__options.triggerClose.mouseleave
+ || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
+ ) {
+
+ // we use an event to allow users/plugins to control when the mouseleave/touchleave
+ // close triggers will come to action. It allows to have more triggering elements
+ // than just the origin and the tooltip for example, or to cancel/delay the closing,
+ // or to make the tooltip interactive even if it wasn't when it was open, etc.
+ self._on('dismissable', function(event) {
+
+ if (event.dismissable) {
+
+ if (event.delay) {
+
+ timeout = setTimeout(function() {
+ // event.event may be undefined
+ self._close(event.event);
+ }, event.delay);
+
+ self.__timeouts.close.push(timeout);
+ }
+ else {
+ self._close(event);
+ }
+ }
+ else {
+ clearTimeout(timeout);
+ }
+ });
+
+ // now set the listeners that will trigger 'dismissable' events
+ var $elements = self._$origin,
+ eventNamesIn = '',
+ eventNamesOut = '',
+ timeout = null;
+
+ // if we have to allow interaction, bind on the tooltip too
+ if (self.__options.interactive) {
+ $elements = $elements.add(self._$tooltip);
+ }
+
+ if (self.__options.triggerClose.mouseleave) {
+ eventNamesIn += 'mouseenter.'+ self.__namespace +'-triggerClose ';
+ eventNamesOut += 'mouseleave.'+ self.__namespace +'-triggerClose ';
+ }
+ if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
+ eventNamesIn += 'touchstart.'+ self.__namespace +'-triggerClose';
+ eventNamesOut += 'touchend.'+ self.__namespace +'-triggerClose touchcancel.'+ self.__namespace +'-triggerClose';
+ }
+
+ $elements
+ // close after some time spent outside of the elements
+ .on(eventNamesOut, function(event) {
+
+ // it's ok if the touch gesture ended up to be a swipe,
+ // it's still a "touch leave" situation
+ if ( self._touchIsTouchEvent(event)
+ || !self._touchIsEmulatedEvent(event)
+ ) {
+
+ var delay = (event.type == 'mouseleave') ?
+ self.__options.delay :
+ self.__options.delayTouch;
+
+ self._trigger({
+ delay: delay[1],
+ dismissable: true,
+ event: event,
+ type: 'dismissable'
+ });
+ }
+ })
+ // suspend the mouseleave timeout when the pointer comes back
+ // over the elements
+ .on(eventNamesIn, function(event) {
+
+ // it's also ok if the touch event is a swipe gesture
+ if ( self._touchIsTouchEvent(event)
+ || !self._touchIsEmulatedEvent(event)
+ ) {
+ self._trigger({
+ dismissable: false,
+ event: event,
+ type: 'dismissable'
+ });
+ }
+ });
+ }
+
+ // close the tooltip when the origin gets a mouse click (common behavior of
+ // native tooltips)
+ if (self.__options.triggerClose.originClick) {
+
+ self._$origin.on('click.'+ self.__namespace + '-triggerClose', function(event) {
+
+ // we could actually let a tap trigger this but this feature just
+ // does not make sense on touch devices
+ if ( !self._touchIsTouchEvent(event)
+ && !self._touchIsEmulatedEvent(event)
+ ) {
+ self._close(event);
+ }
+ });
+ }
+
+ // set the same bindings for click and touch on the body to close the tooltip
+ if ( self.__options.triggerClose.click
+ || (self.__options.triggerClose.tap && env.hasTouchCapability)
+ ) {
+
+ // don't set right away since the click/tap event which triggered this method
+ // (if it was a click/tap) is going to bubble up to the body, we don't want it
+ // to close the tooltip immediately after it opened
+ setTimeout(function() {
+
+ if (self.__state != 'closed') {
+
+ var eventNames = '';
+ if (self.__options.triggerClose.click) {
+ eventNames += 'click.'+ self.__namespace +'-triggerClose ';
+ }
+ if (self.__options.triggerClose.tap && env.hasTouchCapability) {
+ eventNames += 'touchend.'+ self.__namespace +'-triggerClose';
+ }
+
+ $('body').on(eventNames, function(event) {
+
+ if (self._touchIsMeaningfulEvent(event)) {
+
+ self._touchRecordEvent(event);
+
+ if (!self.__options.interactive || !$.contains(self._$tooltip[0], event.target)) {
+ self._close(event);
+ }
+ }
+ });
+
+ // needed to detect and ignore swiping
+ if (self.__options.triggerClose.tap && env.hasTouchCapability) {
+
+ $('body').on('touchstart.'+ self.__namespace +'-triggerClose', function(event) {
+ self._touchRecordEvent(event);
+ });
+ }
+ }
+ }, 0);
+ }
+
+ self._trigger('ready');
+
+ // call our custom callback
+ if (self.__options.functionReady) {
+ self.__options.functionReady.call(self, self, {
+ origin: self._$origin[0],
+ tooltip: self._$tooltip[0]
+ });
+ }
+ }
+
+ // if we have a timer set, let the countdown begin
+ if (self.__options.timer > 0) {
+
+ var timeout = setTimeout(function() {
+ self._close();
+ }, self.__options.timer + extraTime);
+
+ self.__timeouts.close.push(timeout);
+ }
+ }
+ }
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * When using the mouseenter/touchstart open triggers, this function will
+ * schedule the opening of the tooltip after the delay, if there is one
+ *
+ * @param event
+ * @returns {self}
+ * @protected
+ */
+ _openShortly: function(event) {
+
+ var self = this,
+ ok = true;
+
+ if (self.__state != 'stable' && self.__state != 'appearing') {
+
+ // if a timeout is not already running
+ if (!self.__timeouts.open) {
+
+ self._trigger({
+ type: 'start',
+ event: event,
+ stop: function() {
+ ok = false;
+ }
+ });
+
+ if (ok) {
+
+ var delay = (event.type.indexOf('touch') == 0) ?
+ self.__options.delayTouch :
+ self.__options.delay;
+
+ if (delay[0]) {
+
+ self.__timeouts.open = setTimeout(function() {
+
+ self.__timeouts.open = null;
+
+ // open only if the pointer (mouse or touch) is still over the origin.
+ // The check on the "meaningful event" can only be made here, after some
+ // time has passed (to know if the touch was a swipe or not)
+ if (self.__pointerIsOverOrigin && self._touchIsMeaningfulEvent(event)) {
+
+ // signal that we go on
+ self._trigger('startend');
+
+ self._open(event);
+ }
+ else {
+ // signal that we cancel
+ self._trigger('startcancel');
+ }
+ }, delay[0]);
+ }
+ else {
+ // signal that we go on
+ self._trigger('startend');
+
+ self._open(event);
+ }
+ }
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * Meant for plugins to get their options
+ *
+ * @param {string} pluginName The name of the plugin that asks for its options
+ * @param {object} defaultOptions The default options of the plugin
+ * @returns {object} The options
+ * @protected
+ */
+ _optionsExtract: function(pluginName, defaultOptions) {
+
+ var self = this,
+ options = $.extend(true, {}, defaultOptions);
+
+ // if the plugin options were isolated in a property named after the
+ // plugin, use them (prevents conflicts with other plugins)
+ var pluginOptions = self.__options[pluginName];
+
+ // if not, try to get them as regular options
+ if (!pluginOptions){
+
+ pluginOptions = {};
+
+ $.each(defaultOptions, function(optionName, value) {
+
+ var o = self.__options[optionName];
+
+ if (o !== undefined) {
+ pluginOptions[optionName] = o;
+ }
+ });
+ }
+
+ // let's merge the default options and the ones that were provided. We'd want
+ // to do a deep copy but not let jQuery merge arrays, so we'll do a shallow
+ // extend on two levels, that will be enough if options are not more than 1
+ // level deep
+ $.each(options, function(optionName, value) {
+
+ if (pluginOptions[optionName] !== undefined) {
+
+ if (( typeof value == 'object'
+ && !(value instanceof Array)
+ && value != null
+ )
+ &&
+ ( typeof pluginOptions[optionName] == 'object'
+ && !(pluginOptions[optionName] instanceof Array)
+ && pluginOptions[optionName] != null
+ )
+ ) {
+ $.extend(options[optionName], pluginOptions[optionName]);
+ }
+ else {
+ options[optionName] = pluginOptions[optionName];
+ }
+ }
+ });
+
+ return options;
+ },
+
+ /**
+ * Used at instantiation of the plugin, or afterwards by plugins that activate themselves
+ * on existing instances
+ *
+ * @param {object} pluginName
+ * @returns {self}
+ * @protected
+ */
+ _plug: function(pluginName) {
+
+ var plugin = $.tooltipster._plugin(pluginName);
+
+ if (plugin) {
+
+ // if there is a constructor for instances
+ if (plugin.instance) {
+
+ // proxy non-private methods on the instance to allow new instance methods
+ $.tooltipster.__bridge(plugin.instance, this, plugin.name);
+ }
+ }
+ else {
+ throw new Error('The "'+ pluginName +'" plugin is not defined');
+ }
+
+ return this;
+ },
+
+ /**
+ * This will return true if the event is a mouse event which was
+ * emulated by the browser after a touch event. This allows us to
+ * really dissociate mouse and touch triggers.
+ *
+ * There is a margin of error if a real mouse event is fired right
+ * after (within the delay shown below) a touch event on the same
+ * element, but hopefully it should not happen often.
+ *
+ * @returns {boolean}
+ * @protected
+ */
+ _touchIsEmulatedEvent: function(event) {
+
+ var isEmulated = false,
+ now = new Date().getTime();
+
+ for (var i = this.__touchEvents.length - 1; i >= 0; i--) {
+
+ var e = this.__touchEvents[i];
+
+ // delay, in milliseconds. It's supposed to be 300ms in
+ // most browsers (350ms on iOS) to allow a double tap but
+ // can be less (check out FastClick for more info)
+ if (now - e.time < 500) {
+
+ if (e.target === event.target) {
+ isEmulated = true;
+ }
+ }
+ else {
+ break;
+ }
+ }
+
+ return isEmulated;
+ },
+
+ /**
+ * Returns false if the event was an emulated mouse event or
+ * a touch event involved in a swipe gesture.
+ *
+ * @param {object} event
+ * @returns {boolean}
+ * @protected
+ */
+ _touchIsMeaningfulEvent: function(event) {
+ return (
+ (this._touchIsTouchEvent(event) && !this._touchSwiped(event.target))
+ || (!this._touchIsTouchEvent(event) && !this._touchIsEmulatedEvent(event))
+ );
+ },
+
+ /**
+ * Checks if an event is a touch event
+ *
+ * @param {object} event
+ * @returns {boolean}
+ * @protected
+ */
+ _touchIsTouchEvent: function(event){
+ return event.type.indexOf('touch') == 0;
+ },
+
+ /**
+ * Store touch events for a while to detect swiping and emulated mouse events
+ *
+ * @param {object} event
+ * @returns {self}
+ * @protected
+ */
+ _touchRecordEvent: function(event) {
+
+ if (this._touchIsTouchEvent(event)) {
+ event.time = new Date().getTime();
+ this.__touchEvents.push(event);
+ }
+
+ return this;
+ },
+
+ /**
+ * Returns true if a swipe happened after the last touchstart event fired on
+ * event.target.
+ *
+ * We need to differentiate a swipe from a tap before we let the event open
+ * or close the tooltip. A swipe is when a touchmove (scroll) event happens
+ * on the body between the touchstart and the touchend events of an element.
+ *
+ * @param {object} target The HTML element that may have triggered the swipe
+ * @returns {boolean}
+ * @protected
+ */
+ _touchSwiped: function(target) {
+
+ var swiped = false;
+
+ for (var i = this.__touchEvents.length - 1; i >= 0; i--) {
+
+ var e = this.__touchEvents[i];
+
+ if (e.type == 'touchmove') {
+ swiped = true;
+ break;
+ }
+ else if (
+ e.type == 'touchstart'
+ && target === e.target
+ ) {
+ break;
+ }
+ }
+
+ return swiped;
+ },
+
+ /**
+ * Triggers an event on the instance emitters
+ *
+ * @returns {self}
+ * @protected
+ */
+ _trigger: function() {
+
+ var args = Array.prototype.slice.apply(arguments);
+
+ if (typeof args[0] == 'string') {
+ args[0] = { type: args[0] };
+ }
+
+ // add properties to the event
+ args[0].instance = this;
+ args[0].origin = this._$origin ? this._$origin[0] : null;
+ args[0].tooltip = this._$tooltip ? this._$tooltip[0] : null;
+
+ // note: the order of emitters matters
+ this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
+ $.tooltipster._trigger.apply($.tooltipster, args);
+ this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);
+
+ return this;
+ },
+
+ /**
+ * Deactivate a plugin on this instance
+ *
+ * @returns {self}
+ * @protected
+ */
+ _unplug: function(pluginName) {
+
+ var self = this;
+
+ // if the plugin has been activated on this instance
+ if (self[pluginName]) {
+
+ var plugin = $.tooltipster._plugin(pluginName);
+
+ // if there is a constructor for instances
+ if (plugin.instance) {
+
+ // unbridge
+ $.each(plugin.instance, function(methodName, fn) {
+
+ // if the method exists (privates methods do not) and comes indeed from
+ // this plugin (may be missing or come from a conflicting plugin).
+ if ( self[methodName]
+ && self[methodName].bridged === self[pluginName]
+ ) {
+ delete self[methodName];
+ }
+ });
+ }
+
+ // destroy the plugin
+ if (self[pluginName].__destroy) {
+ self[pluginName].__destroy();
+ }
+
+ // remove the reference to the plugin instance
+ delete self[pluginName];
+ }
+
+ return self;
+ },
+
+ /**
+ * @see self::_close
+ * @returns {self}
+ * @public
+ */
+ close: function(callback) {
+
+ if (!this.__destroyed) {
+ this._close(null, callback);
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ },
+
+ /**
+ * Sets or gets the content of the tooltip
+ *
+ * @returns {mixed|self}
+ * @public
+ */
+ content: function(content) {
+
+ var self = this;
+
+ // getter method
+ if (content === undefined) {
+ return self.__Content;
+ }
+ // setter method
+ else {
+
+ if (!self.__destroyed) {
+
+ // change the content
+ self.__contentSet(content);
+
+ if (self.__Content !== null) {
+
+ // update the tooltip if it is open
+ if (self.__state !== 'closed') {
+
+ // reset the content in the tooltip
+ self.__contentInsert();
+
+ // reposition and resize the tooltip
+ self.reposition();
+
+ // if we want to play a little animation showing the content changed
+ if (self.__options.updateAnimation) {
+
+ if (env.hasTransitions) {
+
+ // keep the reference in the local scope
+ var animation = self.__options.updateAnimation;
+
+ self._$tooltip.addClass('tooltipster-update-'+ animation);
+
+ // remove the class after a while. The actual duration of the
+ // update animation may be shorter, it's set in the CSS rules
+ setTimeout(function() {
+
+ if (self.__state != 'closed') {
+
+ self._$tooltip.removeClass('tooltipster-update-'+ animation);
+ }
+ }, 1000);
+ }
+ else {
+ self._$tooltip.fadeTo(200, 0.5, function() {
+ if (self.__state != 'closed') {
+ self._$tooltip.fadeTo(200, 1);
+ }
+ });
+ }
+ }
+ }
+ }
+ else {
+ self._close();
+ }
+ }
+ else {
+ self.__destroyError();
+ }
+
+ return self;
+ }
+ },
+
+ /**
+ * Destroys the tooltip
+ *
+ * @returns {self}
+ * @public
+ */
+ destroy: function() {
+
+ var self = this;
+
+ if (!self.__destroyed) {
+
+ if (!self.__destroying) {
+
+ self.__destroying = true;
+
+ self._close(null, function() {
+
+ self._trigger('destroy');
+
+ self.__destroying = false;
+ self.__destroyed = true;
+
+ self._$origin
+ .removeData(self.__namespace)
+ // remove the open trigger listeners
+ .off('.'+ self.__namespace +'-triggerOpen');
+
+ // remove the touch listener
+ $('body').off('.' + self.__namespace +'-triggerOpen');
+
+ var ns = self._$origin.data('tooltipster-ns');
+
+ // if the origin has been removed from DOM, its data may
+ // well have been destroyed in the process and there would
+ // be nothing to clean up or restore
+ if (ns) {
+
+ // if there are no more tooltips on this element
+ if (ns.length === 1) {
+
+ // optional restoration of a title attribute
+ var title = null;
+ if (self.__options.restoration == 'previous') {
+ title = self._$origin.data('tooltipster-initialTitle');
+ }
+ else if (self.__options.restoration == 'current') {
+
+ // old school technique to stringify when outerHTML is not supported
+ title = (typeof self.__Content == 'string') ?
+ self.__Content :
+ $('').append(self.__Content).html();
+ }
+
+ if (title) {
+ self._$origin.attr('title', title);
+ }
+
+ // final cleaning
+
+ self._$origin.removeClass('tooltipstered');
+
+ self._$origin
+ .removeData('tooltipster-ns')
+ .removeData('tooltipster-initialTitle');
+ }
+ else {
+ // remove the instance namespace from the list of namespaces of
+ // tooltips present on the element
+ ns = $.grep(ns, function(el, i) {
+ return el !== self.__namespace;
+ });
+ self._$origin.data('tooltipster-ns', ns);
+ }
+ }
+
+ // last event
+ self._trigger('destroyed');
+
+ // unbind private and public event listeners
+ self._off();
+ self.off();
+
+ // remove external references, just in case
+ self.__Content = null;
+ self.__$emitterPrivate = null;
+ self.__$emitterPublic = null;
+ self.__options.parent = null;
+ self._$origin = null;
+ self._$tooltip = null;
+
+ // make sure the object is no longer referenced in there to prevent
+ // memory leaks
+ $.tooltipster.__instancesLatestArr = $.grep($.tooltipster.__instancesLatestArr, function(el, i) {
+ return self !== el;
+ });
+
+ clearInterval(self.__garbageCollector);
+ });
+ }
+ }
+ else {
+ self.__destroyError();
+ }
+
+ // we return the scope rather than true so that the call to
+ // .tooltipster('destroy') actually returns the matched elements
+ // and applies to all of them
+ return self;
+ },
+
+ /**
+ * Disables the tooltip
+ *
+ * @returns {self}
+ * @public
+ */
+ disable: function() {
+
+ if (!this.__destroyed) {
+
+ // close first, in case the tooltip would not disappear on
+ // its own (no close trigger)
+ this._close();
+ this.__enabled = false;
+
+ return this;
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ },
+
+ /**
+ * Returns the HTML element of the origin
+ *
+ * @returns {self}
+ * @public
+ */
+ elementOrigin: function() {
+
+ if (!this.__destroyed) {
+ return this._$origin[0];
+ }
+ else {
+ this.__destroyError();
+ }
+ },
+
+ /**
+ * Returns the HTML element of the tooltip
+ *
+ * @returns {self}
+ * @public
+ */
+ elementTooltip: function() {
+ return this._$tooltip ? this._$tooltip[0] : null;
+ },
+
+ /**
+ * Enables the tooltip
+ *
+ * @returns {self}
+ * @public
+ */
+ enable: function() {
+ this.__enabled = true;
+ return this;
+ },
+
+ /**
+ * Alias, deprecated in 4.0.0
+ *
+ * @param {function} callback
+ * @returns {self}
+ * @public
+ */
+ hide: function(callback) {
+ return this.close(callback);
+ },
+
+ /**
+ * Returns the instance
+ *
+ * @returns {self}
+ * @public
+ */
+ instance: function() {
+ return this;
+ },
+
+ /**
+ * For public use only, not to be used by plugins (use ::_off() instead)
+ *
+ * @returns {self}
+ * @public
+ */
+ off: function() {
+
+ if (!this.__destroyed) {
+ this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ }
+
+ return this;
+ },
+
+ /**
+ * For public use only, not to be used by plugins (use ::_on() instead)
+ *
+ * @returns {self}
+ * @public
+ */
+ on: function() {
+
+ if (!this.__destroyed) {
+ this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ },
+
+ /**
+ * For public use only, not to be used by plugins
+ *
+ * @returns {self}
+ * @public
+ */
+ one: function() {
+
+ if (!this.__destroyed) {
+ this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ },
+
+ /**
+ * @see self::_open
+ * @returns {self}
+ * @public
+ */
+ open: function(callback) {
+
+ if (!this.__destroyed && !this.__destroying) {
+ this._open(null, callback);
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ },
+
+ /**
+ * Get or set options. For internal use and advanced users only.
+ *
+ * @param {string} o Option name
+ * @param {mixed} val optional A new value for the option
+ * @return {mixed|self} If val is omitted, the value of the option
+ * is returned, otherwise the instance itself is returned
+ * @public
+ */
+ option: function(o, val) {
+
+ // getter
+ if (val === undefined) {
+ return this.__options[o];
+ }
+ // setter
+ else {
+
+ if (!this.__destroyed) {
+
+ // change value
+ this.__options[o] = val;
+
+ // format
+ this.__optionsFormat();
+
+ // re-prepare the triggers if needed
+ if ($.inArray(o, ['trigger', 'triggerClose', 'triggerOpen']) >= 0) {
+ this.__prepareOrigin();
+ }
+
+ if (o === 'selfDestruction') {
+ this.__prepareGC();
+ }
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ }
+ },
+
+ /**
+ * This method is in charge of setting the position and size properties of the tooltip.
+ * All the hard work is delegated to the display plugin.
+ * Note: The tooltip may be detached from the DOM at the moment the method is called
+ * but must be attached by the end of the method call.
+ *
+ * @param {object} event For internal use only. Defined if an event such as
+ * window resizing triggered the repositioning
+ * @param {boolean} tooltipIsDetached For internal use only. Set this to true if you
+ * know that the tooltip not being in the DOM is not an issue (typically when the
+ * tooltip element has just been created but has not been added to the DOM yet).
+ * @returns {self}
+ * @public
+ */
+ reposition: function(event, tooltipIsDetached) {
+
+ var self = this;
+
+ if (!self.__destroyed) {
+
+ // if the tooltip has not been removed from DOM manually (or if it
+ // has been detached on purpose)
+ if (bodyContains(self._$tooltip) || tooltipIsDetached) {
+
+ if (!tooltipIsDetached) {
+ // detach in case the tooltip overflows the window and adds
+ // scrollbars to it, so __geometry can be accurate
+ self._$tooltip.detach();
+ }
+
+ // refresh the geometry object before passing it as a helper
+ self.__Geometry = self.__geometry();
+
+ // let a plugin fo the rest
+ self._trigger({
+ type: 'reposition',
+ event: event,
+ helper: {
+ geo: self.__Geometry
+ }
+ });
+ }
+ }
+ else {
+ self.__destroyError();
+ }
+
+ return self;
+ },
+
+ /**
+ * Alias, deprecated in 4.0.0
+ *
+ * @param callback
+ * @returns {self}
+ * @public
+ */
+ show: function(callback) {
+ return this.open(callback);
+ },
+
+ /**
+ * Returns some properties about the instance
+ *
+ * @returns {object}
+ * @public
+ */
+ status: function() {
+
+ return {
+ destroyed: this.__destroyed,
+ destroying: this.__destroying,
+ enabled: this.__enabled,
+ open: this.__state !== 'closed',
+ state: this.__state
+ };
+ },
+
+ /**
+ * For public use only, not to be used by plugins
+ *
+ * @returns {self}
+ * @public
+ */
+ triggerHandler: function() {
+
+ if (!this.__destroyed) {
+ this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ }
+};
+
+$.fn.tooltipster = function() {
+
+ // for using in closures
+ var args = Array.prototype.slice.apply(arguments),
+ // common mistake: an HTML element can't be in several tooltips at the same time
+ contentCloningWarning = 'You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.';
+
+ // this happens with $(sel).tooltipster(...) when $(sel) does not match anything
+ if (this.length === 0) {
+
+ // still chainable
+ return this;
+ }
+ // this happens when calling $(sel).tooltipster('methodName or options')
+ // where $(sel) matches one or more elements
+ else {
+
+ // method calls
+ if (typeof args[0] === 'string') {
+
+ var v = '#*$~&';
+
+ this.each(function() {
+
+ // retrieve the namepaces of the tooltip(s) that exist on that element.
+ // We will interact with the first tooltip only.
+ var ns = $(this).data('tooltipster-ns'),
+ // self represents the instance of the first tooltipster plugin
+ // associated to the current HTML object of the loop
+ self = ns ? $(this).data(ns[0]) : null;
+
+ // if the current element holds a tooltipster instance
+ if (self) {
+
+ if (typeof self[args[0]] === 'function') {
+
+ if ( this.length > 1
+ && args[0] == 'content'
+ && ( args[1] instanceof $
+ || (typeof args[1] == 'object' && args[1] != null && args[1].tagName)
+ )
+ && !self.__options.contentCloning
+ && self.__options.debug
+ ) {
+ console.log(contentCloningWarning);
+ }
+
+ // note : args[1] and args[2] may not be defined
+ var resp = self[args[0]](args[1], args[2]);
+ }
+ else {
+ throw new Error('Unknown method "'+ args[0] +'"');
+ }
+
+ // if the function returned anything other than the instance
+ // itself (which implies chaining, except for the `instance` method)
+ if (resp !== self || args[0] === 'instance') {
+
+ v = resp;
+
+ // return false to stop .each iteration on the first element
+ // matched by the selector
+ return false;
+ }
+ }
+ else {
+ throw new Error('You called Tooltipster\'s "'+ args[0] +'" method on an uninitialized element');
+ }
+ });
+
+ return (v !== '#*$~&') ? v : this;
+ }
+ // first argument is undefined or an object: the tooltip is initializing
+ else {
+
+ // reset the array of last initialized objects
+ $.tooltipster.__instancesLatestArr = [];
+
+ // is there a defined value for the multiple option in the options object ?
+ var multipleIsSet = args[0] && args[0].multiple !== undefined,
+ // if the multiple option is set to true, or if it's not defined but
+ // set to true in the defaults
+ multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple),
+ // same for content
+ contentIsSet = args[0] && args[0].content !== undefined,
+ content = (contentIsSet && args[0].content) || (!contentIsSet && defaults.content),
+ // same for contentCloning
+ contentCloningIsSet = args[0] && args[0].contentCloning !== undefined,
+ contentCloning =
+ (contentCloningIsSet && args[0].contentCloning)
+ || (!contentCloningIsSet && defaults.contentCloning),
+ // same for debug
+ debugIsSet = args[0] && args[0].debug !== undefined,
+ debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug);
+
+ if ( this.length > 1
+ && ( content instanceof $
+ || (typeof content == 'object' && content != null && content.tagName)
+ )
+ && !contentCloning
+ && debug
+ ) {
+ console.log(contentCloningWarning);
+ }
+
+ // create a tooltipster instance for each element if it doesn't
+ // already have one or if the multiple option is set, and attach the
+ // object to it
+ this.each(function() {
+
+ var go = false,
+ $this = $(this),
+ ns = $this.data('tooltipster-ns'),
+ obj = null;
+
+ if (!ns) {
+ go = true;
+ }
+ else if (multiple) {
+ go = true;
+ }
+ else if (debug) {
+ console.log('Tooltipster: one or more tooltips are already attached to the element below. Ignoring.');
+ console.log(this);
+ }
+
+ if (go) {
+ obj = new $.Tooltipster(this, args[0]);
+
+ // save the reference of the new instance
+ if (!ns) ns = [];
+ ns.push(obj.__namespace);
+ $this.data('tooltipster-ns', ns);
+
+ // save the instance itself
+ $this.data(obj.__namespace, obj);
+
+ // call our constructor custom function.
+ // we do this here and not in ::init() because we wanted
+ // the object to be saved in $this.data before triggering
+ // it
+ if (obj.__options.functionInit) {
+ obj.__options.functionInit.call(obj, obj, {
+ origin: this
+ });
+ }
+
+ // and now the event, for the plugins and core emitter
+ obj._trigger('init');
+ }
+
+ $.tooltipster.__instancesLatestArr.push(obj);
+ });
+
+ return this;
+ }
+ }
+};
+
+// Utilities
+
+/**
+ * A class to check if a tooltip can fit in given dimensions
+ *
+ * @param {object} $tooltip The jQuery wrapped tooltip element, or a clone of it
+ */
+function Ruler($tooltip) {
+
+ // list of instance variables
+
+ this.$container;
+ this.constraints = null;
+ this.__$tooltip;
+
+ this.__init($tooltip);
+}
+
+Ruler.prototype = {
+
+ /**
+ * Move the tooltip into an invisible div that does not allow overflow to make
+ * size tests. Note: the tooltip may or may not be attached to the DOM at the
+ * moment this method is called, it does not matter.
+ *
+ * @param {object} $tooltip The object to test. May be just a clone of the
+ * actual tooltip.
+ * @private
+ */
+ __init: function($tooltip) {
+
+ this.__$tooltip = $tooltip;
+
+ this.__$tooltip
+ .css({
+ // for some reason we have to specify top and left 0
+ left: 0,
+ // any overflow will be ignored while measuring
+ overflow: 'hidden',
+ // positions at (0,0) without the div using 100% of the available width
+ position: 'absolute',
+ top: 0
+ })
+ // overflow must be auto during the test. We re-set this in case
+ // it were modified by the user
+ .find('.tooltipster-content')
+ .css('overflow', 'auto');
+
+ this.$container = $('')
+ .append(this.__$tooltip)
+ .appendTo('body');
+ },
+
+ /**
+ * Force the browser to redraw (re-render) the tooltip immediately. This is required
+ * when you changed some CSS properties and need to make something with it
+ * immediately, without waiting for the browser to redraw at the end of instructions.
+ *
+ * @see http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes
+ * @private
+ */
+ __forceRedraw: function() {
+
+ // note: this would work but for Webkit only
+ //this.__$tooltip.close();
+ //this.__$tooltip[0].offsetHeight;
+ //this.__$tooltip.open();
+
+ // works in FF too
+ var $p = this.__$tooltip.parent();
+ this.__$tooltip.detach();
+ this.__$tooltip.appendTo($p);
+ },
+
+ /**
+ * Set maximum dimensions for the tooltip. A call to ::measure afterwards
+ * will tell us if the content overflows or if it's ok
+ *
+ * @param {int} width
+ * @param {int} height
+ * @return {Ruler}
+ * @public
+ */
+ constrain: function(width, height) {
+
+ this.constraints = {
+ width: width,
+ height: height
+ };
+
+ this.__$tooltip.css({
+ // we disable display:flex, otherwise the content would overflow without
+ // creating horizontal scrolling (which we need to detect).
+ display: 'block',
+ // reset any previous height
+ height: '',
+ // we'll check if horizontal scrolling occurs
+ overflow: 'auto',
+ // we'll set the width and see what height is generated and if there
+ // is horizontal overflow
+ width: width
+ });
+
+ return this;
+ },
+
+ /**
+ * Reset the tooltip content overflow and remove the test container
+ *
+ * @returns {Ruler}
+ * @public
+ */
+ destroy: function() {
+
+ // in case the element was not a clone
+ this.__$tooltip
+ .detach()
+ .find('.tooltipster-content')
+ .css({
+ // reset to CSS value
+ display: '',
+ overflow: ''
+ });
+
+ this.$container.remove();
+ },
+
+ /**
+ * Removes any constraints
+ *
+ * @returns {Ruler}
+ * @public
+ */
+ free: function() {
+
+ this.constraints = null;
+
+ // reset to natural size
+ this.__$tooltip.css({
+ display: '',
+ height: '',
+ overflow: 'visible',
+ width: ''
+ });
+
+ return this;
+ },
+
+ /**
+ * Returns the size of the tooltip. When constraints are applied, also returns
+ * whether the tooltip fits in the provided dimensions.
+ * The idea is to see if the new height is small enough and if the content does
+ * not overflow horizontally.
+ *
+ * @param {int} width
+ * @param {int} height
+ * @returns {object} An object with a bool `fits` property and a `size` property
+ * @public
+ */
+ measure: function() {
+
+ this.__forceRedraw();
+
+ var tooltipBcr = this.__$tooltip[0].getBoundingClientRect(),
+ result = { size: {
+ // bcr.width/height are not defined in IE8- but in this
+ // case, bcr.right/bottom will have the same value
+ // except in iOS 8+ where tooltipBcr.bottom/right are wrong
+ // after scrolling for reasons yet to be determined
+ height: tooltipBcr.height || tooltipBcr.bottom,
+ width: tooltipBcr.width || tooltipBcr.right
+ }};
+
+ if (this.constraints) {
+
+ // note: we used to use offsetWidth instead of boundingRectClient but
+ // it returned rounded values, causing issues with sub-pixel layouts.
+
+ // note2: noticed that the bcrWidth of text content of a div was once
+ // greater than the bcrWidth of its container by 1px, causing the final
+ // tooltip box to be too small for its content. However, evaluating
+ // their widths one against the other (below) surprisingly returned
+ // equality. Happened only once in Chrome 48, was not able to reproduce
+ // => just having fun with float position values...
+
+ var $content = this.__$tooltip.find('.tooltipster-content'),
+ height = this.__$tooltip.outerHeight(),
+ contentBcr = $content[0].getBoundingClientRect(),
+ fits = {
+ height: height <= this.constraints.height,
+ width: (
+ // this condition accounts for min-width property that
+ // may apply
+ tooltipBcr.width <= this.constraints.width
+ // the -1 is here because scrollWidth actually returns
+ // a rounded value, and may be greater than bcr.width if
+ // it was rounded up. This may cause an issue for contents
+ // which actually really overflow by 1px or so, but that
+ // should be rare. Not sure how to solve this efficiently.
+ // See http://blogs.msdn.com/b/ie/archive/2012/02/17/sub-pixel-rendering-and-the-css-object-model.aspx
+ && contentBcr.width >= $content[0].scrollWidth - 1
+ )
+ };
+
+ result.fits = fits.height && fits.width;
+ }
+
+ // old versions of IE get the width wrong for some reason
+ if (env.IE && env.IE <= 11) {
+ result.size.width = Math.ceil(result.size.width) + 1;
+ }
+
+ return result;
+ }
+};
+
+// quick & dirty compare function, not bijective nor multidimensional
+function areEqual(a,b) {
+ var same = true;
+ $.each(a, function(i, _) {
+ if (b[i] === undefined || a[i] !== b[i]) {
+ same = false;
+ return false;
+ }
+ });
+ return same;
+}
+
+/**
+ * A fast function to check if an element is still in the DOM. It
+ * tries to use an id as ids are indexed by the browser, or falls
+ * back to jQuery's `contains` method. May fail if two elements
+ * have the same id, but so be it
+ *
+ * @param {object} $obj A jQuery-wrapped HTML element
+ * @return {boolean}
+ */
+function bodyContains($obj) {
+ var id = $obj.attr('id'),
+ el = id ? env.window.document.getElementById(id) : null;
+ // must also check that the element with the id is the one we want
+ return el ? el === $obj[0] : $.contains(env.window.document.body, $obj[0]);
+}
+
+// detect IE versions for dirty fixes
+var uA = navigator.userAgent.toLowerCase();
+if (uA.indexOf('msie') != -1) env.IE = parseInt(uA.split('msie')[1]);
+else if (uA.toLowerCase().indexOf('trident') !== -1 && uA.indexOf(' rv:11') !== -1) env.IE = 11;
+else if (uA.toLowerCase().indexOf('edge/') != -1) env.IE = parseInt(uA.toLowerCase().split('edge/')[1]);
+
+// detecting support for CSS transitions
+function transitionSupport() {
+
+ // env.window is not defined yet when this is called
+ if (!win) return false;
+
+ var b = win.document.body || win.document.documentElement,
+ s = b.style,
+ p = 'transition',
+ v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'];
+
+ if (typeof s[p] == 'string') { return true; }
+
+ p = p.charAt(0).toUpperCase() + p.substr(1);
+ for (var i=0; i' +
+ '
' +
+ '' +
+ '
' +
+ '
' +
+ '
' +
+ '' +
+ '' +
+ '
' +
+ '
' +
+ ''
+ );
+
+ // hide arrow if asked
+ if (!this.__options.arrow) {
+ $html
+ .find('.tooltipster-box')
+ .css('margin', 0)
+ .end()
+ .find('.tooltipster-arrow')
+ .hide();
+ }
+
+ // apply min/max width if asked
+ if (this.__options.minWidth) {
+ $html.css('min-width', this.__options.minWidth + 'px');
+ }
+ if (this.__options.maxWidth) {
+ $html.css('max-width', this.__options.maxWidth + 'px');
+ }
+
+ this.__instance._$tooltip = $html;
+
+ // tell the instance that the tooltip element has been created
+ this.__instance._trigger('created');
+ },
+
+ /**
+ * Used when the plugin is to be unplugged
+ *
+ * @private
+ */
+ __destroy: function() {
+ this.__instance._off('.'+ self.__namespace);
+ },
+
+ /**
+ * (Re)compute this.__options from the options declared to the instance
+ *
+ * @private
+ */
+ __optionsFormat: function() {
+
+ var self = this;
+
+ // get the options
+ self.__options = self.__instance._optionsExtract(pluginName, self.__defaults());
+
+ // for backward compatibility, deprecated in v4.0.0
+ if (self.__options.position) {
+ self.__options.side = self.__options.position;
+ }
+
+ // options formatting
+
+ // format distance as a four-cell array if it ain't one yet and then make
+ // it an object with top/bottom/left/right properties
+ if (typeof self.__options.distance != 'object') {
+ self.__options.distance = [self.__options.distance];
+ }
+ if (self.__options.distance.length < 4) {
+
+ if (self.__options.distance[1] === undefined) self.__options.distance[1] = self.__options.distance[0];
+ if (self.__options.distance[2] === undefined) self.__options.distance[2] = self.__options.distance[0];
+ if (self.__options.distance[3] === undefined) self.__options.distance[3] = self.__options.distance[1];
+
+ self.__options.distance = {
+ top: self.__options.distance[0],
+ right: self.__options.distance[1],
+ bottom: self.__options.distance[2],
+ left: self.__options.distance[3]
+ };
+ }
+
+ // let's transform:
+ // 'top' into ['top', 'bottom', 'right', 'left']
+ // 'right' into ['right', 'left', 'top', 'bottom']
+ // 'bottom' into ['bottom', 'top', 'right', 'left']
+ // 'left' into ['left', 'right', 'top', 'bottom']
+ if (typeof self.__options.side == 'string') {
+
+ var opposites = {
+ 'top': 'bottom',
+ 'right': 'left',
+ 'bottom': 'top',
+ 'left': 'right'
+ };
+
+ self.__options.side = [self.__options.side, opposites[self.__options.side]];
+
+ if (self.__options.side[0] == 'left' || self.__options.side[0] == 'right') {
+ self.__options.side.push('top', 'bottom');
+ }
+ else {
+ self.__options.side.push('right', 'left');
+ }
+ }
+
+ // misc
+ // disable the arrow in IE6 unless the arrow option was explicitly set to true
+ if ( $.tooltipster._env.IE === 6
+ && self.__options.arrow !== true
+ ) {
+ self.__options.arrow = false;
+ }
+ },
+
+ /**
+ * This method must compute and set the positioning properties of the
+ * tooltip (left, top, width, height, etc.). It must also make sure the
+ * tooltip is eventually appended to its parent (since the element may be
+ * detached from the DOM at the moment the method is called).
+ *
+ * We'll evaluate positioning scenarios to find which side can contain the
+ * tooltip in the best way. We'll consider things relatively to the window
+ * (unless the user asks not to), then to the document (if need be, or if the
+ * user explicitly requires the tests to run on the document). For each
+ * scenario, measures are taken, allowing us to know how well the tooltip
+ * is going to fit. After that, a sorting function will let us know what
+ * the best scenario is (we also allow the user to choose his favorite
+ * scenario by using an event).
+ *
+ * @param {object} helper An object that contains variables that plugin
+ * creators may find useful (see below)
+ * @param {object} helper.geo An object with many layout properties
+ * about objects of interest (window, document, origin). This should help
+ * plugin users compute the optimal position of the tooltip
+ * @private
+ */
+ __reposition: function(event, helper) {
+
+ var self = this,
+ finalResult,
+ // to know where to put the tooltip, we need to know on which point
+ // of the x or y axis we should center it. That coordinate is the target
+ targets = self.__targetFind(helper),
+ testResults = [];
+
+ // make sure the tooltip is detached while we make tests on a clone
+ self.__instance._$tooltip.detach();
+
+ // we could actually provide the original element to the Ruler and
+ // not a clone, but it just feels right to keep it out of the
+ // machinery.
+ var $clone = self.__instance._$tooltip.clone(),
+ // start position tests session
+ ruler = $.tooltipster._getRuler($clone),
+ satisfied = false;
+
+ // start evaluating scenarios
+ $.each(['window', 'document'], function(i, container) {
+
+ var takeTest = null;
+
+ // let the user decide to keep on testing or not
+ self.__instance._trigger({
+ container: container,
+ helper: helper,
+ satisfied: satisfied,
+ takeTest: function(bool) {
+ takeTest = bool;
+ },
+ results: testResults,
+ type: 'positionTest'
+ });
+
+ if ( takeTest == true
+ || ( takeTest != false
+ && satisfied == false
+ // skip the window scenarios if asked. If they are reintegrated by
+ // the callback of the positionTest event, they will have to be
+ // excluded using the callback of positionTested
+ && (container != 'window' || self.__options.viewportAware)
+ )
+ ) {
+
+ // for each allowed side
+ for (var i=0; i < self.__options.side.length; i++) {
+
+ var distance = {
+ horizontal: 0,
+ vertical: 0
+ },
+ side = self.__options.side[i];
+
+ if (side == 'top' || side == 'bottom') {
+ distance.vertical = self.__options.distance[side];
+ }
+ else {
+ distance.horizontal = self.__options.distance[side];
+ }
+
+ // this may have an effect on the size of the tooltip if there are css
+ // rules for the arrow or something else
+ self.__sideChange($clone, side);
+
+ $.each(['natural', 'constrained'], function(i, mode) {
+
+ takeTest = null;
+
+ // emit an event on the instance
+ self.__instance._trigger({
+ container: container,
+ event: event,
+ helper: helper,
+ mode: mode,
+ results: testResults,
+ satisfied: satisfied,
+ side: side,
+ takeTest: function(bool) {
+ takeTest = bool;
+ },
+ type: 'positionTest'
+ });
+
+ if ( takeTest == true
+ || ( takeTest != false
+ && satisfied == false
+ )
+ ) {
+
+ var testResult = {
+ container: container,
+ // we let the distance as an object here, it can make things a little easier
+ // during the user's calculations at positionTest/positionTested
+ distance: distance,
+ // whether the tooltip can fit in the size of the viewport (does not mean
+ // that we'll be able to make it initially entirely visible, see 'whole')
+ fits: null,
+ mode: mode,
+ outerSize: null,
+ side: side,
+ size: null,
+ target: targets[side],
+ // check if the origin has enough surface on screen for the tooltip to
+ // aim at it without overflowing the viewport (this is due to the thickness
+ // of the arrow represented by the minIntersection length).
+ // If not, the tooltip will have to be partly or entirely off screen in
+ // order to stay docked to the origin. This value will stay null when the
+ // container is the document, as it is not relevant
+ whole: null
+ };
+
+ // get the size of the tooltip with or without size constraints
+ var rulerConfigured = (mode == 'natural') ?
+ ruler.free() :
+ ruler.constrain(
+ helper.geo.available[container][side].width - distance.horizontal,
+ helper.geo.available[container][side].height - distance.vertical
+ ),
+ rulerResults = rulerConfigured.measure();
+
+ testResult.size = rulerResults.size;
+ testResult.outerSize = {
+ height: rulerResults.size.height + distance.vertical,
+ width: rulerResults.size.width + distance.horizontal
+ };
+
+ if (mode == 'natural') {
+
+ if( helper.geo.available[container][side].width >= testResult.outerSize.width
+ && helper.geo.available[container][side].height >= testResult.outerSize.height
+ ) {
+ testResult.fits = true;
+ }
+ else {
+ testResult.fits = false;
+ }
+ }
+ else {
+ testResult.fits = rulerResults.fits;
+ }
+
+ if (container == 'window') {
+
+ if (!testResult.fits) {
+ testResult.whole = false;
+ }
+ else {
+ if (side == 'top' || side == 'bottom') {
+
+ testResult.whole = (
+ helper.geo.origin.windowOffset.right >= self.__options.minIntersection
+ && helper.geo.window.size.width - helper.geo.origin.windowOffset.left >= self.__options.minIntersection
+ );
+ }
+ else {
+ testResult.whole = (
+ helper.geo.origin.windowOffset.bottom >= self.__options.minIntersection
+ && helper.geo.window.size.height - helper.geo.origin.windowOffset.top >= self.__options.minIntersection
+ );
+ }
+ }
+ }
+
+ testResults.push(testResult);
+
+ // we don't need to compute more positions if we have one fully on screen
+ if (testResult.whole) {
+ satisfied = true;
+ }
+ else {
+ // don't run the constrained test unless the natural width was greater
+ // than the available width, otherwise it's pointless as we know it
+ // wouldn't fit either
+ if ( testResult.mode == 'natural'
+ && ( testResult.fits
+ || testResult.size.width <= helper.geo.available[container][side].width
+ )
+ ) {
+ return false;
+ }
+ }
+ }
+ });
+ }
+ }
+ });
+
+ // the user may eliminate the unwanted scenarios from testResults, but he's
+ // not supposed to alter them at this point. functionPosition and the
+ // position event serve that purpose.
+ self.__instance._trigger({
+ edit: function(r) {
+ testResults = r;
+ },
+ event: event,
+ helper: helper,
+ results: testResults,
+ type: 'positionTested'
+ });
+
+ /**
+ * Sort the scenarios to find the favorite one.
+ *
+ * The favorite scenario is when we can fully display the tooltip on screen,
+ * even if it means that the middle of the tooltip is no longer centered on
+ * the middle of the origin (when the origin is near the edge of the screen
+ * or even partly off screen). We want the tooltip on the preferred side,
+ * even if it means that we have to use a constrained size rather than a
+ * natural one (as long as it fits). When the origin is off screen at the top
+ * the tooltip will be positioned at the bottom (if allowed), if the origin
+ * is off screen on the right, it will be positioned on the left, etc.
+ * If there are no scenarios where the tooltip can fit on screen, or if the
+ * user does not want the tooltip to fit on screen (viewportAware == false),
+ * we fall back to the scenarios relative to the document.
+ *
+ * When the tooltip is bigger than the viewport in either dimension, we stop
+ * looking at the window scenarios and consider the document scenarios only,
+ * with the same logic to find on which side it would fit best.
+ *
+ * If the tooltip cannot fit the document on any side, we force it at the
+ * bottom, so at least the user can scroll to see it.
+ */
+ testResults.sort(function(a, b) {
+
+ // best if it's whole (the tooltip fits and adapts to the viewport)
+ if (a.whole && !b.whole) {
+ return -1;
+ }
+ else if (!a.whole && b.whole) {
+ return 1;
+ }
+ else if (a.whole && b.whole) {
+
+ var ai = self.__options.side.indexOf(a.side),
+ bi = self.__options.side.indexOf(b.side);
+
+ // use the user's sides fallback array
+ if (ai < bi) {
+ return -1;
+ }
+ else if (ai > bi) {
+ return 1;
+ }
+ else {
+ // will be used if the user forced the tests to continue
+ return a.mode == 'natural' ? -1 : 1;
+ }
+ }
+ else {
+
+ // better if it fits
+ if (a.fits && !b.fits) {
+ return -1;
+ }
+ else if (!a.fits && b.fits) {
+ return 1;
+ }
+ else if (a.fits && b.fits) {
+
+ var ai = self.__options.side.indexOf(a.side),
+ bi = self.__options.side.indexOf(b.side);
+
+ // use the user's sides fallback array
+ if (ai < bi) {
+ return -1;
+ }
+ else if (ai > bi) {
+ return 1;
+ }
+ else {
+ // will be used if the user forced the tests to continue
+ return a.mode == 'natural' ? -1 : 1;
+ }
+ }
+ else {
+
+ // if everything failed, this will give a preference to the case where
+ // the tooltip overflows the document at the bottom
+ if ( a.container == 'document'
+ && a.side == 'bottom'
+ && a.mode == 'natural'
+ ) {
+ return -1;
+ }
+ else {
+ return 1;
+ }
+ }
+ }
+ });
+
+ finalResult = testResults[0];
+
+
+ // now let's find the coordinates of the tooltip relatively to the window
+ finalResult.coord = {};
+
+ switch (finalResult.side) {
+
+ case 'left':
+ case 'right':
+ finalResult.coord.top = Math.floor(finalResult.target - finalResult.size.height / 2);
+ break;
+
+ case 'bottom':
+ case 'top':
+ finalResult.coord.left = Math.floor(finalResult.target - finalResult.size.width / 2);
+ break;
+ }
+
+ switch (finalResult.side) {
+
+ case 'left':
+ finalResult.coord.left = helper.geo.origin.windowOffset.left - finalResult.outerSize.width;
+ break;
+
+ case 'right':
+ finalResult.coord.left = helper.geo.origin.windowOffset.right + finalResult.distance.horizontal;
+ break;
+
+ case 'top':
+ finalResult.coord.top = helper.geo.origin.windowOffset.top - finalResult.outerSize.height;
+ break;
+
+ case 'bottom':
+ finalResult.coord.top = helper.geo.origin.windowOffset.bottom + finalResult.distance.vertical;
+ break;
+ }
+
+ // if the tooltip can potentially be contained within the viewport dimensions
+ // and that we are asked to make it fit on screen
+ if (finalResult.container == 'window') {
+
+ // if the tooltip overflows the viewport, we'll move it accordingly (then it will
+ // not be centered on the middle of the origin anymore). We only move horizontally
+ // for top and bottom tooltips and vice versa.
+ if (finalResult.side == 'top' || finalResult.side == 'bottom') {
+
+ // if there is an overflow on the left
+ if (finalResult.coord.left < 0) {
+
+ // prevent the overflow unless the origin itself gets off screen (minus the
+ // margin needed to keep the arrow pointing at the target)
+ if (helper.geo.origin.windowOffset.right - this.__options.minIntersection >= 0) {
+ finalResult.coord.left = 0;
+ }
+ else {
+ finalResult.coord.left = helper.geo.origin.windowOffset.right - this.__options.minIntersection - 1;
+ }
+ }
+ // or an overflow on the right
+ else if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) {
+
+ if (helper.geo.origin.windowOffset.left + this.__options.minIntersection <= helper.geo.window.size.width) {
+ finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width;
+ }
+ else {
+ finalResult.coord.left = helper.geo.origin.windowOffset.left + this.__options.minIntersection + 1 - finalResult.size.width;
+ }
+ }
+ }
+ else {
+
+ // overflow at the top
+ if (finalResult.coord.top < 0) {
+
+ if (helper.geo.origin.windowOffset.bottom - this.__options.minIntersection >= 0) {
+ finalResult.coord.top = 0;
+ }
+ else {
+ finalResult.coord.top = helper.geo.origin.windowOffset.bottom - this.__options.minIntersection - 1;
+ }
+ }
+ // or at the bottom
+ else if (finalResult.coord.top > helper.geo.window.size.height - finalResult.size.height) {
+
+ if (helper.geo.origin.windowOffset.top + this.__options.minIntersection <= helper.geo.window.size.height) {
+ finalResult.coord.top = helper.geo.window.size.height - finalResult.size.height;
+ }
+ else {
+ finalResult.coord.top = helper.geo.origin.windowOffset.top + this.__options.minIntersection + 1 - finalResult.size.height;
+ }
+ }
+ }
+ }
+ else {
+
+ // there might be overflow here too but it's easier to handle. If there has
+ // to be an overflow, we'll make sure it's on the right side of the screen
+ // (because the browser will extend the document size if there is an overflow
+ // on the right, but not on the left). The sort function above has already
+ // made sure that a bottom document overflow is preferred to a top overflow,
+ // so we don't have to care about it.
+
+ // if there is an overflow on the right
+ if (finalResult.coord.left > helper.geo.window.size.width - finalResult.size.width) {
+
+ // this may actually create on overflow on the left but we'll fix it in a sec
+ finalResult.coord.left = helper.geo.window.size.width - finalResult.size.width;
+ }
+
+ // if there is an overflow on the left
+ if (finalResult.coord.left < 0) {
+
+ // don't care if it overflows the right after that, we made our best
+ finalResult.coord.left = 0;
+ }
+ }
+
+
+ // submit the positioning proposal to the user function which may choose to change
+ // the side, size and/or the coordinates
+
+ // first, set the rules that corresponds to the proposed side: it may change
+ // the size of the tooltip, and the custom functionPosition may want to detect the
+ // size of something before making a decision. So let's make things easier for the
+ // implementor
+ self.__sideChange($clone, finalResult.side);
+
+ // add some variables to the helper
+ helper.tooltipClone = $clone[0];
+ helper.tooltipParent = self.__instance.option('parent').parent[0];
+ // move informative values to the helper
+ helper.mode = finalResult.mode;
+ helper.whole = finalResult.whole;
+ // add some variables to the helper for the functionPosition callback (these
+ // will also be added to the event fired by self.__instance._trigger but that's
+ // ok, we're just being consistent)
+ helper.origin = self.__instance._$origin[0];
+ helper.tooltip = self.__instance._$tooltip[0];
+
+ // leave only the actionable values in there for functionPosition
+ delete finalResult.container;
+ delete finalResult.fits;
+ delete finalResult.mode;
+ delete finalResult.outerSize;
+ delete finalResult.whole;
+
+ // keep only the distance on the relevant side, for clarity
+ finalResult.distance = finalResult.distance.horizontal || finalResult.distance.vertical;
+
+ // beginners may not be comfortable with the concept of editing the object
+ // passed by reference, so we provide an edit function and pass a clone
+ var finalResultClone = $.extend(true, {}, finalResult);
+
+ // emit an event on the instance
+ self.__instance._trigger({
+ edit: function(result) {
+ finalResult = result;
+ },
+ event: event,
+ helper: helper,
+ position: finalResultClone,
+ type: 'position'
+ });
+
+ if (self.__options.functionPosition) {
+
+
+ var result = self.__options.functionPosition.call(self, self.__instance, helper, finalResultClone);
+
+ if (result) finalResult = result;
+ }
+
+ // end the positioning tests session (the user might have had a
+ // use for it during the position event, now it's over)
+ ruler.destroy();
+
+
+ // compute the position of the target relatively to the tooltip root
+ // element so we can place the arrow and make the needed adjustments
+ var arrowCoord,
+ maxVal;
+
+ if (finalResult.side == 'top' || finalResult.side == 'bottom') {
+
+ arrowCoord = {
+ prop: 'left',
+ val: finalResult.target - finalResult.coord.left
+ };
+ maxVal = finalResult.size.width - this.__options.minIntersection;
+ }
+ else {
+
+ arrowCoord = {
+ prop: 'top',
+ val: finalResult.target - finalResult.coord.top
+ };
+ maxVal = finalResult.size.height - this.__options.minIntersection;
+ }
+
+ // cannot lie beyond the boundaries of the tooltip, minus the
+ // arrow margin
+ if (arrowCoord.val < this.__options.minIntersection) {
+ arrowCoord.val = this.__options.minIntersection;
+ }
+ else if (arrowCoord.val > maxVal) {
+ arrowCoord.val = maxVal;
+ }
+
+ var originParentOffset;
+
+ // let's convert the window-relative coordinates into coordinates relative to the
+ // future positioned parent that the tooltip will be appended to
+ if (helper.geo.origin.fixedLineage) {
+
+ // same as windowOffset when the position is fixed
+ originParentOffset = helper.geo.origin.windowOffset;
+ }
+ else {
+
+ // this assumes that the parent of the tooltip is located at
+ // (0, 0) in the document, typically like when the parent is
+ // .
+ // If we ever allow other types of parent, .tooltipster-ruler
+ // will have to be appended to the parent to inherit css style
+ // values that affect the display of the text and such.
+ originParentOffset = {
+ left: helper.geo.origin.windowOffset.left + helper.geo.window.scroll.left,
+ top: helper.geo.origin.windowOffset.top + helper.geo.window.scroll.top
+ };
+ }
+
+ finalResult.coord = {
+ left: originParentOffset.left + (finalResult.coord.left - helper.geo.origin.windowOffset.left),
+ top: originParentOffset.top + (finalResult.coord.top - helper.geo.origin.windowOffset.top)
+ };
+
+ // set position values on the original tooltip element
+
+ self.__sideChange(self.__instance._$tooltip, finalResult.side);
+
+ if (helper.geo.origin.fixedLineage) {
+ self.__instance._$tooltip
+ .css('position', 'fixed');
+ }
+ else {
+ // CSS default
+ self.__instance._$tooltip
+ .css('position', '');
+ }
+
+ self.__instance._$tooltip
+ .css({
+ left: finalResult.coord.left,
+ top: finalResult.coord.top,
+ // we need to set a size even if the tooltip is in its natural size
+ // because when the tooltip is positioned beyond the width of the body
+ // (which is by default the width of the window; it will happen when
+ // you scroll the window horizontally to get to the origin), its text
+ // content will otherwise break lines at each word to keep up with the
+ // body overflow strategy.
+ height: finalResult.size.height,
+ width: finalResult.size.width
+ })
+ .find('.tooltipster-arrow')
+ .css({
+ 'left': '',
+ 'top': ''
+ })
+ .css(arrowCoord.prop, arrowCoord.val);
+
+ // append the tooltip HTML element to its parent
+ self.__instance._$tooltip.appendTo(self.__instance.option('parent'));
+
+ self.__instance._trigger({
+ type: 'repositioned',
+ event: event,
+ position: finalResult
+ });
+ },
+
+ /**
+ * Make whatever modifications are needed when the side is changed. This has
+ * been made an independant method for easy inheritance in custom plugins based
+ * on this default plugin.
+ *
+ * @param {object} $obj
+ * @param {string} side
+ * @private
+ */
+ __sideChange: function($obj, side) {
+
+ $obj
+ .removeClass('tooltipster-bottom')
+ .removeClass('tooltipster-left')
+ .removeClass('tooltipster-right')
+ .removeClass('tooltipster-top')
+ .addClass('tooltipster-'+ side);
+ },
+
+ /**
+ * Returns the target that the tooltip should aim at for a given side.
+ * The calculated value is a distance from the edge of the window
+ * (left edge for top/bottom sides, top edge for left/right side). The
+ * tooltip will be centered on that position and the arrow will be
+ * positioned there (as much as possible).
+ *
+ * @param {object} helper
+ * @return {integer}
+ * @private
+ */
+ __targetFind: function(helper) {
+
+ var target = {},
+ rects = this.__instance._$origin[0].getClientRects();
+
+ // these lines fix a Chrome bug (issue #491)
+ if (rects.length > 1) {
+ var opacity = this.__instance._$origin.css('opacity');
+ if(opacity == 1) {
+ this.__instance._$origin.css('opacity', 0.99);
+ rects = this.__instance._$origin[0].getClientRects();
+ this.__instance._$origin.css('opacity', 1);
+ }
+ }
+
+ // by default, the target will be the middle of the origin
+ if (rects.length < 2) {
+
+ target.top = Math.floor(helper.geo.origin.windowOffset.left + (helper.geo.origin.size.width / 2));
+ target.bottom = target.top;
+
+ target.left = Math.floor(helper.geo.origin.windowOffset.top + (helper.geo.origin.size.height / 2));
+ target.right = target.left;
+ }
+ // if multiple client rects exist, the element may be text split
+ // up into multiple lines and the middle of the origin may not be
+ // best option anymore. We need to choose the best target client rect
+ else {
+
+ // top: the first
+ var targetRect = rects[0];
+ target.top = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2);
+
+ // right: the middle line, rounded down in case there is an even
+ // number of lines (looks more centered => check out the
+ // demo with 4 split lines)
+ if (rects.length > 2) {
+ targetRect = rects[Math.ceil(rects.length / 2) - 1];
+ }
+ else {
+ targetRect = rects[0];
+ }
+ target.right = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2);
+
+ // bottom: the last
+ targetRect = rects[rects.length - 1];
+ target.bottom = Math.floor(targetRect.left + (targetRect.right - targetRect.left) / 2);
+
+ // left: the middle line, rounded up
+ if (rects.length > 2) {
+ targetRect = rects[Math.ceil((rects.length + 1) / 2) - 1];
+ }
+ else {
+ targetRect = rects[rects.length - 1];
+ }
+
+ target.left = Math.floor(targetRect.top + (targetRect.bottom - targetRect.top) / 2);
+ }
+
+ return target;
+ }
+ }
+});
+
+/* a build task will add "return $;" here */
+return $;
+
+}));
diff --git a/PlexRequests.UI/Content/tooltip/tooltipster.bundle.min.css b/PlexRequests.UI/Content/tooltip/tooltipster.bundle.min.css
new file mode 100644
index 000000000..d8f30feec
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/tooltipster.bundle.min.css
@@ -0,0 +1 @@
+.tooltipster-fall,.tooltipster-grow.tooltipster-show{-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1);-moz-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-ms-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-o-transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-base{display:flex;pointer-events:none;position:absolute}.tooltipster-box{flex:1 1 auto}.tooltipster-content{box-sizing:border-box;max-height:100%;max-width:100%;overflow:auto}.tooltipster-ruler{bottom:0;left:0;overflow:hidden;position:fixed;right:0;top:0;visibility:hidden}.tooltipster-fade{opacity:0;-webkit-transition-property:opacity;-moz-transition-property:opacity;-o-transition-property:opacity;-ms-transition-property:opacity;transition-property:opacity}.tooltipster-fade.tooltipster-show{opacity:1}.tooltipster-grow{-webkit-transform:scale(0,0);-moz-transform:scale(0,0);-o-transform:scale(0,0);-ms-transform:scale(0,0);transform:scale(0,0);-webkit-transition-property:-webkit-transform;-moz-transition-property:-moz-transform;-o-transition-property:-o-transform;-ms-transition-property:-ms-transform;transition-property:transform;-webkit-backface-visibility:hidden}.tooltipster-grow.tooltipster-show{-webkit-transform:scale(1,1);-moz-transform:scale(1,1);-o-transform:scale(1,1);-ms-transform:scale(1,1);transform:scale(1,1);-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-swing{opacity:0;-webkit-transform:rotateZ(4deg);-moz-transform:rotateZ(4deg);-o-transform:rotateZ(4deg);-ms-transform:rotateZ(4deg);transform:rotateZ(4deg);-webkit-transition-property:-webkit-transform,opacity;-moz-transition-property:-moz-transform;-o-transition-property:-o-transform;-ms-transition-property:-ms-transform;transition-property:transform}.tooltipster-swing.tooltipster-show{opacity:1;-webkit-transform:rotateZ(0);-moz-transform:rotateZ(0);-o-transform:rotateZ(0);-ms-transform:rotateZ(0);transform:rotateZ(0);-webkit-transition-timing-function:cubic-bezier(.23,.635,.495,1);-webkit-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);-moz-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);-ms-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);-o-transition-timing-function:cubic-bezier(.23,.635,.495,2.4);transition-timing-function:cubic-bezier(.23,.635,.495,2.4)}.tooltipster-fall{-webkit-transition-property:top;-moz-transition-property:top;-o-transition-property:top;-ms-transition-property:top;transition-property:top;-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-fall.tooltipster-initial{top:0!important}.tooltipster-fall.tooltipster-dying{-webkit-transition-property:all;-moz-transition-property:all;-o-transition-property:all;-ms-transition-property:all;transition-property:all;top:0!important;opacity:0}.tooltipster-slide{-webkit-transition-property:left;-moz-transition-property:left;-o-transition-property:left;-ms-transition-property:left;transition-property:left;-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1);-webkit-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-moz-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-ms-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);-o-transition-timing-function:cubic-bezier(.175,.885,.32,1.15);transition-timing-function:cubic-bezier(.175,.885,.32,1.15)}.tooltipster-slide.tooltipster-initial{left:-40px!important}.tooltipster-slide.tooltipster-dying{-webkit-transition-property:all;-moz-transition-property:all;-o-transition-property:all;-ms-transition-property:all;transition-property:all;left:0!important;opacity:0}@keyframes tooltipster-fading{0%{opacity:0}100%{opacity:1}}.tooltipster-update-fade{animation:tooltipster-fading .4s}@keyframes tooltipster-rotating{25%{transform:rotate(-2deg)}75%{transform:rotate(2deg)}100%{transform:rotate(0)}}.tooltipster-update-rotate{animation:tooltipster-rotating .6s}@keyframes tooltipster-scaling{50%{transform:scale(1.1)}100%{transform:scale(1)}}.tooltipster-update-scale{animation:tooltipster-scaling .6s}.tooltipster-sidetip .tooltipster-box{background:#565656;border:2px solid #000;border-radius:4px}.tooltipster-sidetip.tooltipster-bottom .tooltipster-box{margin-top:8px}.tooltipster-sidetip.tooltipster-left .tooltipster-box{margin-right:8px}.tooltipster-sidetip.tooltipster-right .tooltipster-box{margin-left:8px}.tooltipster-sidetip.tooltipster-top .tooltipster-box{margin-bottom:8px}.tooltipster-sidetip .tooltipster-content{color:#fff;line-height:18px;padding:6px 14px}.tooltipster-sidetip .tooltipster-arrow{overflow:hidden;position:absolute}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow{height:10px;margin-left:-10px;top:0;width:20px}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow{height:20px;margin-top:-10px;right:0;top:0;width:10px}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow{height:20px;margin-top:-10px;left:0;top:0;width:10px}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow{bottom:0;height:10px;margin-left:-10px;width:20px}.tooltipster-sidetip .tooltipster-arrow-background,.tooltipster-sidetip .tooltipster-arrow-border{height:0;position:absolute;width:0}.tooltipster-sidetip .tooltipster-arrow-background{border:10px solid transparent}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-background{border-bottom-color:#565656;left:0;top:3px}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow-background{border-left-color:#565656;left:-3px;top:0}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-background{border-right-color:#565656;left:3px;top:0}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow-background{border-top-color:#565656;left:0;top:-3px}.tooltipster-sidetip .tooltipster-arrow-border{border:10px solid transparent;left:0;top:0}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-border{border-bottom-color:#000}.tooltipster-sidetip.tooltipster-left .tooltipster-arrow-border{border-left-color:#000}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-border{border-right-color:#000}.tooltipster-sidetip.tooltipster-top .tooltipster-arrow-border{border-top-color:#000}.tooltipster-sidetip .tooltipster-arrow-uncropped{position:relative}.tooltipster-sidetip.tooltipster-bottom .tooltipster-arrow-uncropped{top:-10px}.tooltipster-sidetip.tooltipster-right .tooltipster-arrow-uncropped{left:-10px}
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/tooltip/tooltipster.bundle.min.js b/PlexRequests.UI/Content/tooltip/tooltipster.bundle.min.js
new file mode 100644
index 000000000..4275ee017
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/tooltipster.bundle.min.js
@@ -0,0 +1,2 @@
+/*! tooltipster v4.1.2 */!function(a,b){"function"==typeof define&&define.amd?define(["jquery"],function(a){return b(a)}):"object"==typeof exports?module.exports=b(require("jquery")):b(jQuery)}(this,function(a){function b(a){this.$container,this.constraints=null,this.__$tooltip,this.__init(a)}function c(b,c){var d=!0;return a.each(b,function(a,e){return void 0===c[a]||b[a]!==c[a]?(d=!1,!1):void 0}),d}function d(b){var c=b.attr("id"),d=c?h.window.document.getElementById(c):null;return d?d===b[0]:a.contains(h.window.document.body,b[0])}function e(){if(!g)return!1;var a=g.document.body||g.document.documentElement,b=a.style,c="transition",d=["Moz","Webkit","Khtml","O","ms"];if("string"==typeof b[c])return!0;c=c.charAt(0).toUpperCase()+c.substr(1);for(var e=0;e0?e=c.__plugins[d]:a.each(c.__plugins,function(a,b){return b.name.substring(b.name.length-d.length-1)=="."+d?(e=b,!1):void 0}),e}if(b.name.indexOf(".")<0)throw new Error("Plugins must be namespaced");return c.__plugins[b.name]=b,b.core&&c.__bridge(b.core,c,b.name),this},_trigger:function(){var a=Array.prototype.slice.apply(arguments);return"string"==typeof a[0]&&(a[0]={type:a[0]}),this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,a),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,a),this},instances:function(b){var c=[],d=b||".tooltipstered";return a(d).each(function(){var b=a(this),d=b.data("tooltipster-ns");d&&a.each(d,function(a,d){c.push(b.data(d))})}),c},instancesLatest:function(){return this.__instancesLatestArr},off:function(){return this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},origins:function(b){var c=b?b+" ":"";return a(c+".tooltipstered").toArray()},setDefaults:function(b){return a.extend(f,b),this},triggerHandler:function(){return this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.tooltipster=new i,a.Tooltipster=function(b,c){this.__callbacks={close:[],open:[]},this.__closingTime,this.__Content,this.__contentBcr,this.__destroyed=!1,this.__destroying=!1,this.__$emitterPrivate=a({}),this.__$emitterPublic=a({}),this.__enabled=!0,this.__garbageCollector,this.__Geometry,this.__lastPosition,this.__namespace="tooltipster-"+Math.round(1e6*Math.random()),this.__options,this.__$originParents,this.__pointerIsOverOrigin=!1,this.__previousThemes=[],this.__state="closed",this.__timeouts={close:[],open:null},this.__touchEvents=[],this.__tracker=null,this._$origin,this._$tooltip,this.__init(b,c)},a.Tooltipster.prototype={__init:function(b,c){var d=this;if(d._$origin=a(b),d.__options=a.extend(!0,{},f,c),d.__optionsFormat(),!h.IE||h.IE>=d.__options.IEmin){var e=null;if(void 0===d._$origin.data("tooltipster-initialTitle")&&(e=d._$origin.attr("title"),void 0===e&&(e=null),d._$origin.data("tooltipster-initialTitle",e)),null!==d.__options.content)d.__contentSet(d.__options.content);else{var g,i=d._$origin.attr("data-tooltip-content");i&&(g=a(i)),g&&g[0]?d.__contentSet(g.first()):d.__contentSet(e)}d._$origin.removeAttr("title").addClass("tooltipstered"),d.__prepareOrigin(),d.__prepareGC(),a.each(d.__options.plugins,function(a,b){d._plug(b)}),h.hasTouchCapability&&a("body").on("touchmove."+d.__namespace+"-triggerOpen",function(a){d._touchRecordEvent(a)}),d._on("created",function(){d.__prepareTooltip()})._on("repositioned",function(a){d.__lastPosition=a.position})}else d.__options.disabled=!0},__contentInsert:function(){var a=this,b=a._$tooltip.find(".tooltipster-content"),c=a.__Content,d=function(a){c=a};return a._trigger({type:"format",content:a.__Content,format:d}),a.__options.functionFormat&&(c=a.__options.functionFormat.call(a,a,{origin:a._$origin[0]},a.__Content)),"string"!=typeof c||a.__options.contentAsHTML?b.empty().append(c):b.text(c),a},__contentSet:function(b){return b instanceof a&&this.__options.contentCloning&&(b=b.clone(!0)),this.__Content=b,this._trigger({type:"updated",content:b}),this},__destroyError:function(){throw new Error("This tooltip has been destroyed and cannot execute your method call.")},__geometry:function(){var b=this,c=b._$origin,d=b._$origin.is("area");if(d){var e=b._$origin.parent().attr("name");c=a('img[usemap="#'+e+'"]')}var f=c[0].getBoundingClientRect(),g=a(h.window.document),i=a(h.window),j=c,k={available:{document:null,window:null},document:{size:{height:g.height(),width:g.width()}},window:{scroll:{left:h.window.scrollX||h.window.document.documentElement.scrollLeft,top:h.window.scrollY||h.window.document.documentElement.scrollTop},size:{height:i.height(),width:i.width()}},origin:{fixedLineage:!1,offset:{},size:{height:f.bottom-f.top,width:f.right-f.left},usemapImage:d?c[0]:null,windowOffset:{bottom:f.bottom,left:f.left,right:f.right,top:f.top}}};if(d){var l=b._$origin.attr("shape"),m=b._$origin.attr("coords");if(m&&(m=m.split(","),a.map(m,function(a,b){m[b]=parseInt(a)})),"default"!=l)switch(l){case"circle":var n=m[0],o=m[1],p=m[2],q=o-p,r=n-p;k.origin.size.height=2*p,k.origin.size.width=k.origin.size.height,k.origin.windowOffset.left+=r,k.origin.windowOffset.top+=q;break;case"rect":var s=m[0],t=m[1],u=m[2],v=m[3];k.origin.size.height=v-t,k.origin.size.width=u-s,k.origin.windowOffset.left+=s,k.origin.windowOffset.top+=t;break;case"poly":for(var w=0,x=0,y=0,z=0,A="even",B=0;By&&(y=C,0===B&&(w=y)),w>C&&(w=C),A="odd"):(C>z&&(z=C,1==B&&(x=z)),x>C&&(x=C),A="even")}k.origin.size.height=z-x,k.origin.size.width=y-w,k.origin.windowOffset.left+=w,k.origin.windowOffset.top+=x}}var D=function(a){k.origin.size.height=a.height,k.origin.windowOffset.left=a.left,k.origin.windowOffset.top=a.top,k.origin.size.width=a.width};for(b._trigger({type:"geometry",edit:D,geometry:{height:k.origin.size.height,left:k.origin.windowOffset.left,top:k.origin.windowOffset.top,width:k.origin.size.width}}),k.origin.windowOffset.right=k.origin.windowOffset.left+k.origin.size.width,k.origin.windowOffset.bottom=k.origin.windowOffset.top+k.origin.size.height,k.origin.offset.left=k.origin.windowOffset.left+h.window.scrollX,k.origin.offset.top=k.origin.windowOffset.top+h.window.scrollY,k.origin.offset.bottom=k.origin.offset.top+k.origin.size.height,k.origin.offset.right=k.origin.offset.left+k.origin.size.width,k.available.document={bottom:{height:k.document.size.height-k.origin.offset.bottom,width:k.document.size.width},left:{height:k.document.size.height,width:k.origin.offset.left},right:{height:k.document.size.height,width:k.document.size.width-k.origin.offset.right},top:{height:k.origin.offset.top,width:k.document.size.width}},k.available.window={bottom:{height:Math.max(k.window.size.height-Math.max(k.origin.windowOffset.bottom,0),0),width:k.window.size.width},left:{height:k.window.size.height,width:Math.max(k.origin.windowOffset.left,0)},right:{height:k.window.size.height,width:Math.max(k.window.size.width-Math.max(k.origin.windowOffset.right,0),0)},top:{height:Math.max(k.origin.windowOffset.top,0),width:k.window.size.width}};"html"!=j[0].tagName.toLowerCase();){if("fixed"==j.css("position")){k.origin.fixedLineage=!0;break}j=j.parent()}return k},__optionsFormat:function(){return"number"==typeof this.__options.animationDuration&&(this.__options.animationDuration=[this.__options.animationDuration,this.__options.animationDuration]),"number"==typeof this.__options.delay&&(this.__options.delay=[this.__options.delay,this.__options.delay]),"number"==typeof this.__options.delayTouch&&(this.__options.delayTouch=[this.__options.delayTouch,this.__options.delayTouch]),"string"==typeof this.__options.theme&&(this.__options.theme=[this.__options.theme]),"string"==typeof this.__options.parent&&(this.__options.parent=a(this.__options.parent)),"hover"==this.__options.trigger?(this.__options.triggerOpen={mouseenter:!0,touchstart:!0},this.__options.triggerClose={mouseleave:!0,originClick:!0,touchleave:!0}):"click"==this.__options.trigger&&(this.__options.triggerOpen={click:!0,tap:!0},this.__options.triggerClose={click:!0,tap:!0}),this._trigger("options"),this},__prepareGC:function(){var b=this;return b.__options.selfDestruction?b.__garbageCollector=setInterval(function(){var c=(new Date).getTime();b.__touchEvents=a.grep(b.__touchEvents,function(a,b){return c-a.time>6e4}),d(b._$origin)||b.destroy()},2e4):clearInterval(b.__garbageCollector),b},__prepareOrigin:function(){var a=this;if(a._$origin.off("."+a.__namespace+"-triggerOpen"),h.hasTouchCapability&&a._$origin.on("touchstart."+a.__namespace+"-triggerOpen touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen",function(b){a._touchRecordEvent(b)}),a.__options.triggerOpen.click||a.__options.triggerOpen.tap&&h.hasTouchCapability){var b="";a.__options.triggerOpen.click&&(b+="click."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.tap&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&a._open(b)})}if(a.__options.triggerOpen.mouseenter||a.__options.triggerOpen.touchstart&&h.hasTouchCapability){var b="";a.__options.triggerOpen.mouseenter&&(b+="mouseenter."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.touchstart&&h.hasTouchCapability&&(b+="touchstart."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){!a._touchIsTouchEvent(b)&&a._touchIsEmulatedEvent(b)||(a.__pointerIsOverOrigin=!0,a._openShortly(b))})}if(a.__options.triggerClose.mouseleave||a.__options.triggerClose.touchleave&&h.hasTouchCapability){var b="";a.__options.triggerClose.mouseleave&&(b+="mouseleave."+a.__namespace+"-triggerOpen "),a.__options.triggerClose.touchleave&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&(a.__pointerIsOverOrigin=!1)})}return a},__prepareTooltip:function(){var b=this,c=b.__options.interactive?"auto":"";return b._$tooltip.attr("id",b.__namespace).css({"pointer-events":c,zIndex:b.__options.zIndex}),a.each(b.__previousThemes,function(a,c){b._$tooltip.removeClass(c)}),a.each(b.__options.theme,function(a,c){b._$tooltip.addClass(c)}),b.__previousThemes=a.merge([],b.__options.theme),b},__scrollHandler:function(b){var c=this;if(c.__options.triggerClose.scroll)c._close(b);else{if(b.target===h.window.document)c.__Geometry.origin.fixedLineage||c.__options.repositionOnScroll&&c.reposition(b);else{var d=c.__geometry(),e=!1;if("fixed"!=c._$origin.css("position")&&c.__$originParents.each(function(b,c){var f=a(c),g=f.css("overflow-x"),h=f.css("overflow-y");if("visible"!=g||"visible"!=h){var i=c.getBoundingClientRect();if("visible"!=g&&(d.origin.windowOffset.lefti.right))return e=!0,!1;if("visible"!=h&&(d.origin.windowOffset.topi.bottom))return e=!0,!1}return"fixed"==f.css("position")?!1:void 0}),e)c._$tooltip.css("visibility","hidden");else if(c._$tooltip.css("visibility","visible"),c.__options.repositionOnScroll)c.reposition(b);else{var f=d.origin.offset.left-c.__Geometry.origin.offset.left,g=d.origin.offset.top-c.__Geometry.origin.offset.top;c._$tooltip.css({left:c.__lastPosition.coord.left+f,top:c.__lastPosition.coord.top+g})}}c._trigger({type:"scroll",event:b})}return c},__stateSet:function(a){return this.__state=a,this._trigger({type:"state",state:a}),this},__timeoutsClear:function(){return clearTimeout(this.__timeouts.open),this.__timeouts.open=null,a.each(this.__timeouts.close,function(a,b){clearTimeout(b)}),this.__timeouts.close=[],this},__trackerStart:function(){var a=this,b=a._$tooltip.find(".tooltipster-content");return a.__options.trackTooltip&&(a.__contentBcr=b[0].getBoundingClientRect()),a.__tracker=setInterval(function(){if(d(a._$origin)&&d(a._$tooltip)){if(a.__options.trackOrigin){var e=a.__geometry(),f=!1;c(e.origin.size,a.__Geometry.origin.size)&&(a.__Geometry.origin.fixedLineage?c(e.origin.windowOffset,a.__Geometry.origin.windowOffset)&&(f=!0):c(e.origin.offset,a.__Geometry.origin.offset)&&(f=!0)),f||(a.__options.triggerClose.mouseleave?a._close():a.reposition())}if(a.__options.trackTooltip){var g=b[0].getBoundingClientRect();g.height===a.__contentBcr.height&&g.width===a.__contentBcr.width||(a.reposition(),a.__contentBcr=g)}}else a._close()},a.__options.trackerInterval),a},_close:function(b,c){var d=this,e=!0;if(d._trigger({type:"close",event:b,stop:function(){e=!1}}),e||d.__destroying){c&&d.__callbacks.close.push(c),d.__callbacks.open=[],d.__timeoutsClear();var f=function(){a.each(d.__callbacks.close,function(a,c){c.call(d,d,{event:b,origin:d._$origin[0]})}),d.__callbacks.close=[]};if("closed"!=d.__state){var g=!0,i=new Date,j=i.getTime(),k=j+d.__options.animationDuration[1];if("disappearing"==d.__state&&k>d.__closingTime&&(g=!1),g){d.__closingTime=k,"disappearing"!=d.__state&&d.__stateSet("disappearing");var l=function(){clearInterval(d.__tracker),d._trigger({type:"closing",event:b}),d._$tooltip.off("."+d.__namespace+"-triggerClose").removeClass("tooltipster-dying"),a(h.window).off("."+d.__namespace+"-triggerClose"),d.__$originParents.each(function(b,c){a(c).off("scroll."+d.__namespace+"-triggerClose")}),d.__$originParents=null,a("body").off("."+d.__namespace+"-triggerClose"),d._$origin.off("."+d.__namespace+"-triggerClose"),d._off("dismissable"),d.__stateSet("closed"),d._trigger({type:"after",event:b}),d.__options.functionAfter&&d.__options.functionAfter.call(d,d,{event:b}),f()};h.hasTransitions?(d._$tooltip.css({"-moz-animation-duration":d.__options.animationDuration[1]+"ms","-ms-animation-duration":d.__options.animationDuration[1]+"ms","-o-animation-duration":d.__options.animationDuration[1]+"ms","-webkit-animation-duration":d.__options.animationDuration[1]+"ms","animation-duration":d.__options.animationDuration[1]+"ms","transition-duration":d.__options.animationDuration[1]+"ms"}),d._$tooltip.clearQueue().removeClass("tooltipster-show").addClass("tooltipster-dying"),d.__options.animationDuration[1]>0&&d._$tooltip.delay(d.__options.animationDuration[1]),d._$tooltip.queue(l)):d._$tooltip.stop().fadeOut(d.__options.animationDuration[1],l)}}else f()}return d},_off:function(){return this.__$emitterPrivate.off.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_on:function(){return this.__$emitterPrivate.on.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_one:function(){return this.__$emitterPrivate.one.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_open:function(b,c){var e=this;if(!e.__destroying&&d(e._$origin)&&e.__enabled){var f=!0;if("closed"==e.__state&&(e._trigger({type:"before",event:b,stop:function(){f=!1}}),f&&e.__options.functionBefore&&(f=e.__options.functionBefore.call(e,e,{event:b,origin:e._$origin[0]}))),f!==!1&&null!==e.__Content){c&&e.__callbacks.open.push(c),e.__callbacks.close=[],e.__timeoutsClear();var g,i=function(){"stable"!=e.__state&&e.__stateSet("stable"),a.each(e.__callbacks.open,function(a,b){b.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}),e.__callbacks.open=[]};if("closed"!==e.__state)g=0,"disappearing"===e.__state?(e.__stateSet("appearing"),h.hasTransitions?(e._$tooltip.clearQueue().removeClass("tooltipster-dying").addClass("tooltipster-show"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i)):e._$tooltip.stop().fadeIn(i)):"stable"==e.__state&&i();else{if(e.__stateSet("appearing"),g=e.__options.animationDuration[0],e.__contentInsert(),e.reposition(b,!0),h.hasTransitions?(e._$tooltip.addClass("tooltipster-"+e.__options.animation).addClass("tooltipster-initial").css({"-moz-animation-duration":e.__options.animationDuration[0]+"ms","-ms-animation-duration":e.__options.animationDuration[0]+"ms","-o-animation-duration":e.__options.animationDuration[0]+"ms","-webkit-animation-duration":e.__options.animationDuration[0]+"ms","animation-duration":e.__options.animationDuration[0]+"ms","transition-duration":e.__options.animationDuration[0]+"ms"}),setTimeout(function(){"closed"!=e.__state&&(e._$tooltip.addClass("tooltipster-show").removeClass("tooltipster-initial"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i))},0)):e._$tooltip.css("display","none").fadeIn(e.__options.animationDuration[0],i),e.__trackerStart(),a(h.window).on("resize."+e.__namespace+"-triggerClose",function(a){e.reposition(a)}).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)}),e.__$originParents=e._$origin.parents(),e.__$originParents.each(function(b,c){a(c).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)})}),e.__options.triggerClose.mouseleave||e.__options.triggerClose.touchleave&&h.hasTouchCapability){e._on("dismissable",function(a){a.dismissable?a.delay?(m=setTimeout(function(){e._close(a.event)},a.delay),e.__timeouts.close.push(m)):e._close(a):clearTimeout(m)});var j=e._$origin,k="",l="",m=null;e.__options.interactive&&(j=j.add(e._$tooltip)),e.__options.triggerClose.mouseleave&&(k+="mouseenter."+e.__namespace+"-triggerClose ",l+="mouseleave."+e.__namespace+"-triggerClose "),e.__options.triggerClose.touchleave&&h.hasTouchCapability&&(k+="touchstart."+e.__namespace+"-triggerClose",l+="touchend."+e.__namespace+"-triggerClose touchcancel."+e.__namespace+"-triggerClose"),j.on(l,function(a){if(e._touchIsTouchEvent(a)||!e._touchIsEmulatedEvent(a)){var b="mouseleave"==a.type?e.__options.delay:e.__options.delayTouch;e._trigger({delay:b[1],dismissable:!0,event:a,type:"dismissable"})}}).on(k,function(a){!e._touchIsTouchEvent(a)&&e._touchIsEmulatedEvent(a)||e._trigger({dismissable:!1,event:a,type:"dismissable"})})}e.__options.triggerClose.originClick&&e._$origin.on("click."+e.__namespace+"-triggerClose",function(a){e._touchIsTouchEvent(a)||e._touchIsEmulatedEvent(a)||e._close(a)}),(e.__options.triggerClose.click||e.__options.triggerClose.tap&&h.hasTouchCapability)&&setTimeout(function(){if("closed"!=e.__state){var b="";e.__options.triggerClose.click&&(b+="click."+e.__namespace+"-triggerClose "),e.__options.triggerClose.tap&&h.hasTouchCapability&&(b+="touchend."+e.__namespace+"-triggerClose"),a("body").on(b,function(b){e._touchIsMeaningfulEvent(b)&&(e._touchRecordEvent(b),e.__options.interactive&&a.contains(e._$tooltip[0],b.target)||e._close(b))}),e.__options.triggerClose.tap&&h.hasTouchCapability&&a("body").on("touchstart."+e.__namespace+"-triggerClose",function(a){e._touchRecordEvent(a)})}},0),e._trigger("ready"),e.__options.functionReady&&e.__options.functionReady.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}if(e.__options.timer>0){var m=setTimeout(function(){e._close()},e.__options.timer+g);e.__timeouts.close.push(m)}}}return e},_openShortly:function(a){var b=this,c=!0;if("stable"!=b.__state&&"appearing"!=b.__state&&!b.__timeouts.open&&(b._trigger({type:"start",event:a,stop:function(){c=!1}}),c)){var d=0==a.type.indexOf("touch")?b.__options.delayTouch:b.__options.delay;d[0]?b.__timeouts.open=setTimeout(function(){b.__timeouts.open=null,b.__pointerIsOverOrigin&&b._touchIsMeaningfulEvent(a)?(b._trigger("startend"),b._open(a)):b._trigger("startcancel")},d[0]):(b._trigger("startend"),b._open(a))}return b},_optionsExtract:function(b,c){var d=this,e=a.extend(!0,{},c),f=d.__options[b];return f||(f={},a.each(c,function(a,b){var c=d.__options[a];void 0!==c&&(f[a]=c)})),a.each(e,function(b,c){void 0!==f[b]&&("object"!=typeof c||c instanceof Array||null==c||"object"!=typeof f[b]||f[b]instanceof Array||null==f[b]?e[b]=f[b]:a.extend(e[b],f[b]))}),e},_plug:function(b){var c=a.tooltipster._plugin(b);if(!c)throw new Error('The "'+b+'" plugin is not defined');return c.instance&&a.tooltipster.__bridge(c.instance,this,c.name),this},_touchIsEmulatedEvent:function(a){for(var b=!1,c=(new Date).getTime(),d=this.__touchEvents.length-1;d>=0;d--){var e=this.__touchEvents[d];if(!(c-e.time<500))break;e.target===a.target&&(b=!0)}return b},_touchIsMeaningfulEvent:function(a){return this._touchIsTouchEvent(a)&&!this._touchSwiped(a.target)||!this._touchIsTouchEvent(a)&&!this._touchIsEmulatedEvent(a)},_touchIsTouchEvent:function(a){return 0==a.type.indexOf("touch")},_touchRecordEvent:function(a){return this._touchIsTouchEvent(a)&&(a.time=(new Date).getTime(),this.__touchEvents.push(a)),this},_touchSwiped:function(a){for(var b=!1,c=this.__touchEvents.length-1;c>=0;c--){var d=this.__touchEvents[c];if("touchmove"==d.type){b=!0;break}if("touchstart"==d.type&&a===d.target)break}return b},_trigger:function(){var b=Array.prototype.slice.apply(arguments);return"string"==typeof b[0]&&(b[0]={type:b[0]}),b[0].instance=this,b[0].origin=this._$origin?this._$origin[0]:null,b[0].tooltip=this._$tooltip?this._$tooltip[0]:null,this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,b),a.tooltipster._trigger.apply(a.tooltipster,b),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,b),this},_unplug:function(b){var c=this;if(c[b]){var d=a.tooltipster._plugin(b);d.instance&&a.each(d.instance,function(a,d){c[a]&&c[a].bridged===c[b]&&delete c[a]}),c[b].__destroy&&c[b].__destroy(),delete c[b]}return c},close:function(a){return this.__destroyed?this.__destroyError():this._close(null,a),this},content:function(a){var b=this;if(void 0===a)return b.__Content;if(b.__destroyed)b.__destroyError();else if(b.__contentSet(a),null!==b.__Content){if("closed"!==b.__state&&(b.__contentInsert(),b.reposition(),b.__options.updateAnimation))if(h.hasTransitions){var c=b.__options.updateAnimation;b._$tooltip.addClass("tooltipster-update-"+c),setTimeout(function(){"closed"!=b.__state&&b._$tooltip.removeClass("tooltipster-update-"+c)},1e3)}else b._$tooltip.fadeTo(200,.5,function(){"closed"!=b.__state&&b._$tooltip.fadeTo(200,1)})}else b._close();return b},destroy:function(){var b=this;return b.__destroyed?b.__destroyError():b.__destroying||(b.__destroying=!0,b._close(null,function(){b._trigger("destroy"),b.__destroying=!1,b.__destroyed=!0,b._$origin.removeData(b.__namespace).off("."+b.__namespace+"-triggerOpen"),a("body").off("."+b.__namespace+"-triggerOpen");var c=b._$origin.data("tooltipster-ns");if(c)if(1===c.length){var d=null;"previous"==b.__options.restoration?d=b._$origin.data("tooltipster-initialTitle"):"current"==b.__options.restoration&&(d="string"==typeof b.__Content?b.__Content:a("").append(b.__Content).html()),d&&b._$origin.attr("title",d),b._$origin.removeClass("tooltipstered"),b._$origin.removeData("tooltipster-ns").removeData("tooltipster-initialTitle")}else c=a.grep(c,function(a,c){return a!==b.__namespace}),b._$origin.data("tooltipster-ns",c);b._trigger("destroyed"),b._off(),b.off(),b.__Content=null,b.__$emitterPrivate=null,b.__$emitterPublic=null,b.__options.parent=null,b._$origin=null,b._$tooltip=null,a.tooltipster.__instancesLatestArr=a.grep(a.tooltipster.__instancesLatestArr,function(a,c){return b!==a}),clearInterval(b.__garbageCollector)})),b},disable:function(){return this.__destroyed?(this.__destroyError(),this):(this._close(),this.__enabled=!1,this)},elementOrigin:function(){return this.__destroyed?void this.__destroyError():this._$origin[0]},elementTooltip:function(){return this._$tooltip?this._$tooltip[0]:null},enable:function(){return this.__enabled=!0,this},hide:function(a){return this.close(a)},instance:function(){return this},off:function(){return this.__destroyed||this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},open:function(a){return this.__destroyed||this.__destroying?this.__destroyError():this._open(null,a),this},option:function(b,c){return void 0===c?this.__options[b]:(this.__destroyed?this.__destroyError():(this.__options[b]=c,this.__optionsFormat(),a.inArray(b,["trigger","triggerClose","triggerOpen"])>=0&&this.__prepareOrigin(),"selfDestruction"===b&&this.__prepareGC()),this)},reposition:function(a,b){var c=this;return c.__destroyed?c.__destroyError():(d(c._$tooltip)||b)&&(b||c._$tooltip.detach(),c.__Geometry=c.__geometry(),c._trigger({type:"reposition",event:a,helper:{geo:c.__Geometry}})),c},show:function(a){return this.open(a)},status:function(){return{destroyed:this.__destroyed,destroying:this.__destroying,enabled:this.__enabled,open:"closed"!==this.__state,state:this.__state}},triggerHandler:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.fn.tooltipster=function(){var b=Array.prototype.slice.apply(arguments),c="You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.";if(0===this.length)return this;if("string"==typeof b[0]){var d="#*$~&";return this.each(function(){var e=a(this).data("tooltipster-ns"),f=e?a(this).data(e[0]):null;if(!f)throw new Error("You called Tooltipster's \""+b[0]+'" method on an uninitialized element');if("function"!=typeof f[b[0]])throw new Error('Unknown method "'+b[0]+'"');this.length>1&&"content"==b[0]&&(b[1]instanceof a||"object"==typeof b[1]&&null!=b[1]&&b[1].tagName)&&!f.__options.contentCloning&&f.__options.debug&&console.log(c);var g=f[b[0]](b[1],b[2]);return g!==f||"instance"===b[0]?(d=g,!1):void 0}),"#*$~&"!==d?d:this}a.tooltipster.__instancesLatestArr=[];var e=b[0]&&void 0!==b[0].multiple,g=e&&b[0].multiple||!e&&f.multiple,h=b[0]&&void 0!==b[0].content,i=h&&b[0].content||!h&&f.content,j=b[0]&&void 0!==b[0].contentCloning,k=j&&b[0].contentCloning||!j&&f.contentCloning,l=b[0]&&void 0!==b[0].debug,m=l&&b[0].debug||!l&&f.debug;return this.length>1&&(i instanceof a||"object"==typeof i&&null!=i&&i.tagName)&&!k&&m&&console.log(c),this.each(function(){var c=!1,d=a(this),e=d.data("tooltipster-ns"),f=null;e?g?c=!0:m&&(console.log("Tooltipster: one or more tooltips are already attached to the element below. Ignoring."),console.log(this)):c=!0,c&&(f=new a.Tooltipster(this,b[0]),e||(e=[]),e.push(f.__namespace),d.data("tooltipster-ns",e),d.data(f.__namespace,f),f.__options.functionInit&&f.__options.functionInit.call(f,f,{origin:this}),f._trigger("init")),a.tooltipster.__instancesLatestArr.push(f)}),this},b.prototype={__init:function(b){this.__$tooltip=b,this.__$tooltip.css({left:0,overflow:"hidden",position:"absolute",top:0}).find(".tooltipster-content").css("overflow","auto"),this.$container=a('').append(this.__$tooltip).appendTo("body")},__forceRedraw:function(){var a=this.__$tooltip.parent();this.__$tooltip.detach(),this.__$tooltip.appendTo(a)},constrain:function(a,b){return this.constraints={width:a,height:b},this.__$tooltip.css({display:"block",height:"",overflow:"auto",width:a}),this},destroy:function(){this.__$tooltip.detach().find(".tooltipster-content").css({display:"",overflow:""}),this.$container.remove()},free:function(){return this.constraints=null,this.__$tooltip.css({display:"",height:"",overflow:"visible",width:""}),this},measure:function(){this.__forceRedraw();var a=this.__$tooltip[0].getBoundingClientRect(),b={size:{height:a.height||a.bottom,width:a.width||a.right}};if(this.constraints){var c=this.__$tooltip.find(".tooltipster-content"),d=this.__$tooltip.outerHeight(),e=c[0].getBoundingClientRect(),f={height:d<=this.constraints.height,width:a.width<=this.constraints.width&&e.width>=c[0].scrollWidth-1};b.fits=f.height&&f.width}return h.IE&&h.IE<=11&&(b.size.width=Math.ceil(b.size.width)+1),b}};var j=navigator.userAgent.toLowerCase();-1!=j.indexOf("msie")?h.IE=parseInt(j.split("msie")[1]):-1!==j.toLowerCase().indexOf("trident")&&-1!==j.indexOf(" rv:11")?h.IE=11:-1!=j.toLowerCase().indexOf("edge/")&&(h.IE=parseInt(j.toLowerCase().split("edge/")[1]));var k="tooltipster.sideTip";return a.tooltipster._plugin({name:k,instance:{__defaults:function(){return{arrow:!0,distance:6,functionPosition:null,maxWidth:null,minIntersection:16,minWidth:0,position:null,side:"top",viewportAware:!0}},__init:function(a){var b=this;b.__instance=a,b.__namespace="tooltipster-sideTip-"+Math.round(1e6*Math.random()),b.__previousState="closed",b.__options,b.__optionsFormat(),b.__instance._on("state."+b.__namespace,function(a){"closed"==a.state?b.__close():"appearing"==a.state&&"closed"==b.__previousState&&b.__create(),b.__previousState=a.state}),b.__instance._on("options."+b.__namespace,function(){b.__optionsFormat()}),b.__instance._on("reposition."+b.__namespace,function(a){b.__reposition(a.event,a.helper)})},__close:function(){this.__instance.content()instanceof a&&this.__instance.content().detach(),this.__instance._$tooltip.remove(),this.__instance._$tooltip=null},__create:function(){var b=a('
');this.__options.arrow||b.find(".tooltipster-box").css("margin",0).end().find(".tooltipster-arrow").hide(),this.__options.minWidth&&b.css("min-width",this.__options.minWidth+"px"),this.__options.maxWidth&&b.css("max-width",this.__options.maxWidth+"px"),this.__instance._$tooltip=b,this.__instance._trigger("created")},__destroy:function(){this.__instance._off("."+self.__namespace)},__optionsFormat:function(){var b=this;if(b.__options=b.__instance._optionsExtract(k,b.__defaults()),b.__options.position&&(b.__options.side=b.__options.position),"object"!=typeof b.__options.distance&&(b.__options.distance=[b.__options.distance]),b.__options.distance.length<4&&(void 0===b.__options.distance[1]&&(b.__options.distance[1]=b.__options.distance[0]),
+void 0===b.__options.distance[2]&&(b.__options.distance[2]=b.__options.distance[0]),void 0===b.__options.distance[3]&&(b.__options.distance[3]=b.__options.distance[1]),b.__options.distance={top:b.__options.distance[0],right:b.__options.distance[1],bottom:b.__options.distance[2],left:b.__options.distance[3]}),"string"==typeof b.__options.side){var c={top:"bottom",right:"left",bottom:"top",left:"right"};b.__options.side=[b.__options.side,c[b.__options.side]],"left"==b.__options.side[0]||"right"==b.__options.side[0]?b.__options.side.push("top","bottom"):b.__options.side.push("right","left")}6===a.tooltipster._env.IE&&b.__options.arrow!==!0&&(b.__options.arrow=!1)},__reposition:function(b,c){var d,e=this,f=e.__targetFind(c),g=[];e.__instance._$tooltip.detach();var h=e.__instance._$tooltip.clone(),i=a.tooltipster._getRuler(h),j=!1;switch(a.each(["window","document"],function(d,k){var l=null;if(e.__instance._trigger({container:k,helper:c,satisfied:j,takeTest:function(a){l=a},results:g,type:"positionTest"}),1==l||0!=l&&0==j&&("window"!=k||e.__options.viewportAware))for(var d=0;d=h.outerSize.width&&c.geo.available[k][n].height>=h.outerSize.height?h.fits=!0:h.fits=!1:h.fits=p.fits,"window"==k&&(h.fits?"top"==n||"bottom"==n?h.whole=c.geo.origin.windowOffset.right>=e.__options.minIntersection&&c.geo.window.size.width-c.geo.origin.windowOffset.left>=e.__options.minIntersection:h.whole=c.geo.origin.windowOffset.bottom>=e.__options.minIntersection&&c.geo.window.size.height-c.geo.origin.windowOffset.top>=e.__options.minIntersection:h.whole=!1),g.push(h),h.whole)j=!0;else if("natural"==h.mode&&(h.fits||h.size.width<=c.geo.available[k][n].width))return!1}})}}),e.__instance._trigger({edit:function(a){g=a},event:b,helper:c,results:g,type:"positionTested"}),g.sort(function(a,b){if(a.whole&&!b.whole)return-1;if(!a.whole&&b.whole)return 1;if(a.whole&&b.whole){var c=e.__options.side.indexOf(a.side),d=e.__options.side.indexOf(b.side);return d>c?-1:c>d?1:"natural"==a.mode?-1:1}if(a.fits&&!b.fits)return-1;if(!a.fits&&b.fits)return 1;if(a.fits&&b.fits){var c=e.__options.side.indexOf(a.side),d=e.__options.side.indexOf(b.side);return d>c?-1:c>d?1:"natural"==a.mode?-1:1}return"document"==a.container&&"bottom"==a.side&&"natural"==a.mode?-1:1}),d=g[0],d.coord={},d.side){case"left":case"right":d.coord.top=Math.floor(d.target-d.size.height/2);break;case"bottom":case"top":d.coord.left=Math.floor(d.target-d.size.width/2)}switch(d.side){case"left":d.coord.left=c.geo.origin.windowOffset.left-d.outerSize.width;break;case"right":d.coord.left=c.geo.origin.windowOffset.right+d.distance.horizontal;break;case"top":d.coord.top=c.geo.origin.windowOffset.top-d.outerSize.height;break;case"bottom":d.coord.top=c.geo.origin.windowOffset.bottom+d.distance.vertical}"window"==d.container?"top"==d.side||"bottom"==d.side?d.coord.left<0?c.geo.origin.windowOffset.right-this.__options.minIntersection>=0?d.coord.left=0:d.coord.left=c.geo.origin.windowOffset.right-this.__options.minIntersection-1:d.coord.left>c.geo.window.size.width-d.size.width&&(c.geo.origin.windowOffset.left+this.__options.minIntersection<=c.geo.window.size.width?d.coord.left=c.geo.window.size.width-d.size.width:d.coord.left=c.geo.origin.windowOffset.left+this.__options.minIntersection+1-d.size.width):d.coord.top<0?c.geo.origin.windowOffset.bottom-this.__options.minIntersection>=0?d.coord.top=0:d.coord.top=c.geo.origin.windowOffset.bottom-this.__options.minIntersection-1:d.coord.top>c.geo.window.size.height-d.size.height&&(c.geo.origin.windowOffset.top+this.__options.minIntersection<=c.geo.window.size.height?d.coord.top=c.geo.window.size.height-d.size.height:d.coord.top=c.geo.origin.windowOffset.top+this.__options.minIntersection+1-d.size.height):(d.coord.left>c.geo.window.size.width-d.size.width&&(d.coord.left=c.geo.window.size.width-d.size.width),d.coord.left<0&&(d.coord.left=0)),e.__sideChange(h,d.side),c.tooltipClone=h[0],c.tooltipParent=e.__instance.option("parent").parent[0],c.mode=d.mode,c.whole=d.whole,c.origin=e.__instance._$origin[0],c.tooltip=e.__instance._$tooltip[0],delete d.container,delete d.fits,delete d.mode,delete d.outerSize,delete d.whole,d.distance=d.distance.horizontal||d.distance.vertical;var k=a.extend(!0,{},d);if(e.__instance._trigger({edit:function(a){d=a},event:b,helper:c,position:k,type:"position"}),e.__options.functionPosition){var l=e.__options.functionPosition.call(e,e.__instance,c,k);l&&(d=l)}i.destroy();var m,n;"top"==d.side||"bottom"==d.side?(m={prop:"left",val:d.target-d.coord.left},n=d.size.width-this.__options.minIntersection):(m={prop:"top",val:d.target-d.coord.top},n=d.size.height-this.__options.minIntersection),m.valn&&(m.val=n);var o;o=c.geo.origin.fixedLineage?c.geo.origin.windowOffset:{left:c.geo.origin.windowOffset.left+c.geo.window.scroll.left,top:c.geo.origin.windowOffset.top+c.geo.window.scroll.top},d.coord={left:o.left+(d.coord.left-c.geo.origin.windowOffset.left),top:o.top+(d.coord.top-c.geo.origin.windowOffset.top)},e.__sideChange(e.__instance._$tooltip,d.side),c.geo.origin.fixedLineage?e.__instance._$tooltip.css("position","fixed"):e.__instance._$tooltip.css("position",""),e.__instance._$tooltip.css({left:d.coord.left,top:d.coord.top,height:d.size.height,width:d.size.width}).find(".tooltipster-arrow").css({left:"",top:""}).css(m.prop,m.val),e.__instance._$tooltip.appendTo(e.__instance.option("parent")),e.__instance._trigger({type:"repositioned",event:b,position:d})},__sideChange:function(a,b){a.removeClass("tooltipster-bottom").removeClass("tooltipster-left").removeClass("tooltipster-right").removeClass("tooltipster-top").addClass("tooltipster-"+b)},__targetFind:function(a){var b={},c=this.__instance._$origin[0].getClientRects();if(c.length>1){var d=this.__instance._$origin.css("opacity");1==d&&(this.__instance._$origin.css("opacity",.99),c=this.__instance._$origin[0].getClientRects(),this.__instance._$origin.css("opacity",1))}if(c.length<2)b.top=Math.floor(a.geo.origin.windowOffset.left+a.geo.origin.size.width/2),b.bottom=b.top,b.left=Math.floor(a.geo.origin.windowOffset.top+a.geo.origin.size.height/2),b.right=b.left;else{var e=c[0];b.top=Math.floor(e.left+(e.right-e.left)/2),e=c.length>2?c[Math.ceil(c.length/2)-1]:c[0],b.right=Math.floor(e.top+(e.bottom-e.top)/2),e=c[c.length-1],b.bottom=Math.floor(e.left+(e.right-e.left)/2),e=c.length>2?c[Math.ceil((c.length+1)/2)-1]:c[c.length-1],b.left=Math.floor(e.top+(e.bottom-e.top)/2)}return b}}}),a});
\ No newline at end of file
diff --git a/PlexRequests.UI/Content/tooltip/tooltipster.core.css b/PlexRequests.UI/Content/tooltip/tooltipster.core.css
new file mode 100644
index 000000000..0df9fb186
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/tooltipster.core.css
@@ -0,0 +1,231 @@
+/* This is the core CSS of Tooltipster */
+
+/* GENERAL STRUCTURE RULES (do not edit this section) */
+
+.tooltipster-base {
+ /* this ensures that a constrained height set by functionPosition,
+ if greater that the natural height of the tooltip, will be enforced
+ in browsers that support display:flex */
+ display: flex;
+ pointer-events: none;
+ /* this may be overriden in JS for fixed position origins */
+ position: absolute;
+}
+
+.tooltipster-box {
+ /* see .tooltipster-base. flex-shrink 1 is only necessary for IE10-
+ and flex-basis auto for IE11- (at least) */
+ flex: 1 1 auto;
+}
+
+.tooltipster-content {
+ /* prevents an overflow if the user adds padding to the div */
+ box-sizing: border-box;
+ /* these make sure we'll be able to detect any overflow */
+ max-height: 100%;
+ max-width: 100%;
+ overflow: auto;
+}
+
+.tooltipster-ruler {
+ /* these let us test the size of the tooltip without overflowing the window */
+ bottom: 0;
+ left: 0;
+ overflow: hidden;
+ position: fixed;
+ right: 0;
+ top: 0;
+ visibility: hidden;
+}
+
+/* ANIMATIONS */
+
+/* Open/close animations */
+
+/* fade */
+
+.tooltipster-fade {
+ opacity: 0;
+ -webkit-transition-property: opacity;
+ -moz-transition-property: opacity;
+ -o-transition-property: opacity;
+ -ms-transition-property: opacity;
+ transition-property: opacity;
+}
+.tooltipster-fade.tooltipster-show {
+ opacity: 1;
+}
+
+/* grow */
+
+.tooltipster-grow {
+ -webkit-transform: scale(0,0);
+ -moz-transform: scale(0,0);
+ -o-transform: scale(0,0);
+ -ms-transform: scale(0,0);
+ transform: scale(0,0);
+ -webkit-transition-property: -webkit-transform;
+ -moz-transition-property: -moz-transform;
+ -o-transition-property: -o-transform;
+ -ms-transition-property: -ms-transform;
+ transition-property: transform;
+ -webkit-backface-visibility: hidden;
+}
+.tooltipster-grow.tooltipster-show {
+ -webkit-transform: scale(1,1);
+ -moz-transform: scale(1,1);
+ -o-transform: scale(1,1);
+ -ms-transform: scale(1,1);
+ transform: scale(1,1);
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+}
+
+/* swing */
+
+.tooltipster-swing {
+ opacity: 0;
+ -webkit-transform: rotateZ(4deg);
+ -moz-transform: rotateZ(4deg);
+ -o-transform: rotateZ(4deg);
+ -ms-transform: rotateZ(4deg);
+ transform: rotateZ(4deg);
+ -webkit-transition-property: -webkit-transform, opacity;
+ -moz-transition-property: -moz-transform;
+ -o-transition-property: -o-transform;
+ -ms-transition-property: -ms-transform;
+ transition-property: transform;
+}
+.tooltipster-swing.tooltipster-show {
+ opacity: 1;
+ -webkit-transform: rotateZ(0deg);
+ -moz-transform: rotateZ(0deg);
+ -o-transform: rotateZ(0deg);
+ -ms-transform: rotateZ(0deg);
+ transform: rotateZ(0deg);
+ -webkit-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 1);
+ -webkit-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
+ -moz-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
+ -ms-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
+ -o-transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
+ transition-timing-function: cubic-bezier(0.230, 0.635, 0.495, 2.4);
+}
+
+/* fall */
+
+.tooltipster-fall {
+ -webkit-transition-property: top;
+ -moz-transition-property: top;
+ -o-transition-property: top;
+ -ms-transition-property: top;
+ transition-property: top;
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+}
+.tooltipster-fall.tooltipster-initial {
+ top: 0 !important;
+}
+.tooltipster-fall.tooltipster-show {
+}
+.tooltipster-fall.tooltipster-dying {
+ -webkit-transition-property: all;
+ -moz-transition-property: all;
+ -o-transition-property: all;
+ -ms-transition-property: all;
+ transition-property: all;
+ top: 0 !important;
+ opacity: 0;
+}
+
+/* slide */
+
+.tooltipster-slide {
+ -webkit-transition-property: left;
+ -moz-transition-property: left;
+ -o-transition-property: left;
+ -ms-transition-property: left;
+ transition-property: left;
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1);
+ -webkit-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -moz-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -ms-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ -o-transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+ transition-timing-function: cubic-bezier(0.175, 0.885, 0.320, 1.15);
+}
+.tooltipster-slide.tooltipster-initial {
+ left: -40px !important;
+}
+.tooltipster-slide.tooltipster-show {
+}
+.tooltipster-slide.tooltipster-dying {
+ -webkit-transition-property: all;
+ -moz-transition-property: all;
+ -o-transition-property: all;
+ -ms-transition-property: all;
+ transition-property: all;
+ left: 0 !important;
+ opacity: 0;
+}
+
+/* Update animations */
+
+/* We use animations rather than transitions here because
+ transition durations may be specified in the style tag due to
+ animationDuration, and we try to avoid collisions and the use
+ of !important */
+
+/* fade */
+
+@keyframes tooltipster-fading {
+ 0% {
+ opacity: 0;
+ }
+ 100% {
+ opacity: 1;
+ }
+}
+
+.tooltipster-update-fade {
+ animation: tooltipster-fading 400ms;
+}
+
+/* rotate */
+
+@keyframes tooltipster-rotating {
+ 25% {
+ transform: rotate(-2deg);
+ }
+ 75% {
+ transform: rotate(2deg);
+ }
+ 100% {
+ transform: rotate(0);
+ }
+}
+
+.tooltipster-update-rotate {
+ animation: tooltipster-rotating 600ms;
+}
+
+/* scale */
+
+@keyframes tooltipster-scaling {
+ 50% {
+ transform: scale(1.1);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+.tooltipster-update-scale {
+ animation: tooltipster-scaling 600ms;
+}
diff --git a/PlexRequests.UI/Content/tooltip/tooltipster.core.js b/PlexRequests.UI/Content/tooltip/tooltipster.core.js
new file mode 100644
index 000000000..79c59a3f2
--- /dev/null
+++ b/PlexRequests.UI/Content/tooltip/tooltipster.core.js
@@ -0,0 +1,3299 @@
+/**
+ * tooltipster http://iamceege.github.io/tooltipster/
+ * A rockin' custom tooltip jQuery plugin
+ * Developed by Caleb Jacob and Louis Ameline
+ * MIT license
+ */
+(function (root, factory) {
+ if (typeof define === 'function' && define.amd) {
+ // AMD. Register as an anonymous module unless amdModuleId is set
+ define(["jquery"], function (a0) {
+ return (factory(a0));
+ });
+ } else if (typeof exports === 'object') {
+ // Node. Does not work with strict CommonJS, but
+ // only CommonJS-like environments that support module.exports,
+ // like Node.
+ module.exports = factory(require("jquery"));
+ } else {
+ factory(jQuery);
+ }
+}(this, function ($) {
+
+// This file will be UMDified by a build task.
+
+var defaults = {
+ animation: 'fade',
+ animationDuration: 350,
+ content: null,
+ contentAsHTML: false,
+ contentCloning: false,
+ debug: true,
+ delay: 300,
+ delayTouch: [300, 500],
+ functionInit: null,
+ functionBefore: null,
+ functionReady: null,
+ functionAfter: null,
+ functionFormat: null,
+ IEmin: 6,
+ interactive: false,
+ multiple: false,
+ // must be 'body' for now, or an element positioned at (0, 0)
+ // in the document, typically like the very top views of an app.
+ parent: 'body',
+ plugins: ['sideTip'],
+ repositionOnScroll: false,
+ restoration: 'none',
+ selfDestruction: true,
+ theme: [],
+ timer: 0,
+ trackerInterval: 500,
+ trackOrigin: false,
+ trackTooltip: false,
+ trigger: 'hover',
+ triggerClose: {
+ click: false,
+ mouseleave: false,
+ originClick: false,
+ scroll: false,
+ tap: false,
+ touchleave: false
+ },
+ triggerOpen: {
+ click: false,
+ mouseenter: false,
+ tap: false,
+ touchstart: false
+ },
+ updateAnimation: 'rotate',
+ zIndex: 9999999
+ },
+ // we'll avoid using the 'window' global as a good practice but npm's
+ // jquery@<2.1.0 package actually requires a 'window' global, so not sure
+ // it's useful at all
+ win = (typeof window != 'undefined') ? window : null,
+ // env will be proxied by the core for plugins to have access its properties
+ env = {
+ // detect if this device can trigger touch events. Better have a false
+ // positive (unused listeners, that's ok) than a false negative.
+ // https://github.com/Modernizr/Modernizr/blob/master/feature-detects/touchevents.js
+ // http://stackoverflow.com/questions/4817029/whats-the-best-way-to-detect-a-touch-screen-device-using-javascript
+ hasTouchCapability: !!(
+ win
+ && ( 'ontouchstart' in win
+ || (win.DocumentTouch && win.document instanceof win.DocumentTouch)
+ || win.navigator.maxTouchPoints
+ )
+ ),
+ hasTransitions: transitionSupport(),
+ IE: false,
+ // don't set manually, it will be updated by a build task after the manifest
+ semVer: '4.1.2',
+ window: win
+ },
+ core = function() {
+
+ // core variables
+
+ // the core emitters
+ this.__$emitterPrivate = $({});
+ this.__$emitterPublic = $({});
+ this.__instancesLatestArr = [];
+ // collects plugin constructors
+ this.__plugins = {};
+ // proxy env variables for plugins who might use them
+ this._env = env;
+ };
+
+// core methods
+core.prototype = {
+
+ /**
+ * A function to proxy the public methods of an object onto another
+ *
+ * @param {object} constructor The constructor to bridge
+ * @param {object} obj The object that will get new methods (an instance or the core)
+ * @param {string} pluginName A plugin name for the console log message
+ * @return {core}
+ * @private
+ */
+ __bridge: function(constructor, obj, pluginName) {
+
+ // if it's not already bridged
+ if (!obj[pluginName]) {
+
+ var fn = function() {};
+ fn.prototype = constructor;
+
+ var pluginInstance = new fn();
+
+ // the _init method has to exist in instance constructors but might be missing
+ // in core constructors
+ if (pluginInstance.__init) {
+ pluginInstance.__init(obj);
+ }
+
+ $.each(constructor, function(methodName, fn) {
+
+ // don't proxy "private" methods, only "protected" and public ones
+ if (methodName.indexOf('__') != 0) {
+
+ // if the method does not exist yet
+ if (!obj[methodName]) {
+
+ obj[methodName] = function() {
+ return pluginInstance[methodName].apply(pluginInstance, Array.prototype.slice.apply(arguments));
+ };
+
+ // remember to which plugin this method corresponds (several plugins may
+ // have methods of the same name, we need to be sure)
+ obj[methodName].bridged = pluginInstance;
+ }
+ else if (defaults.debug) {
+
+ console.log('The '+ methodName +' method of the '+ pluginName
+ +' plugin conflicts with another plugin or native methods');
+ }
+ }
+ });
+
+ obj[pluginName] = pluginInstance;
+ }
+
+ return this;
+ },
+
+ /**
+ * For mockup in Node env if need be, for testing purposes
+ *
+ * @return {core}
+ * @private
+ */
+ __setWindow: function(window) {
+ env.window = window;
+ return this;
+ },
+
+ /**
+ * Returns a ruler, a tool to help measure the size of a tooltip under
+ * various settings. Meant for plugins
+ *
+ * @see Ruler
+ * @return {object} A Ruler instance
+ * @protected
+ */
+ _getRuler: function($tooltip) {
+ return new Ruler($tooltip);
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @return {core}
+ * @protected
+ */
+ _off: function() {
+ this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @return {core}
+ * @protected
+ */
+ _on: function() {
+ this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @return {core}
+ * @protected
+ */
+ _one: function() {
+ this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * Returns (getter) or adds (setter) a plugin
+ *
+ * @param {string|object} plugin Provide a string (in the full form
+ * "namespace.name") to use as as getter, an object to use as a setter
+ * @return {object|core}
+ * @protected
+ */
+ _plugin: function(plugin) {
+
+ var self = this;
+
+ // getter
+ if (typeof plugin == 'string') {
+
+ var pluginName = plugin,
+ p = null;
+
+ // if the namespace is provided, it's easy to search
+ if (pluginName.indexOf('.') > 0) {
+ p = self.__plugins[pluginName];
+ }
+ // otherwise, return the first name that matches
+ else {
+ $.each(self.__plugins, function(i, plugin) {
+
+ if (plugin.name.substring(plugin.name.length - pluginName.length - 1) == '.'+ pluginName) {
+ p = plugin;
+ return false;
+ }
+ });
+ }
+
+ return p;
+ }
+ // setter
+ else {
+
+ // force namespaces
+ if (plugin.name.indexOf('.') < 0) {
+ throw new Error('Plugins must be namespaced');
+ }
+
+ self.__plugins[plugin.name] = plugin;
+
+ // if the plugin has core features
+ if (plugin.core) {
+
+ // bridge non-private methods onto the core to allow new core methods
+ self.__bridge(plugin.core, self, plugin.name);
+ }
+
+ return this;
+ }
+ },
+
+ /**
+ * Trigger events on the core emitters
+ *
+ * @returns {core}
+ * @protected
+ */
+ _trigger: function() {
+
+ var args = Array.prototype.slice.apply(arguments);
+
+ if (typeof args[0] == 'string') {
+ args[0] = { type: args[0] };
+ }
+
+ // note: the order of emitters matters
+ this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
+ this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);
+
+ return this;
+ },
+
+ /**
+ * Returns instances of all tooltips in the page or an a given element
+ *
+ * @param {string|HTML object collection} selector optional Use this
+ * parameter to restrict the set of objects that will be inspected
+ * for the retrieval of instances. By default, all instances in the
+ * page are returned.
+ * @return {array} An array of instance objects
+ * @public
+ */
+ instances: function(selector) {
+
+ var instances = [],
+ sel = selector || '.tooltipstered';
+
+ $(sel).each(function() {
+
+ var $this = $(this),
+ ns = $this.data('tooltipster-ns');
+
+ if (ns) {
+
+ $.each(ns, function(i, namespace) {
+ instances.push($this.data(namespace));
+ });
+ }
+ });
+
+ return instances;
+ },
+
+ /**
+ * Returns the Tooltipster objects generated by the last initializing call
+ *
+ * @return {array} An array of instance objects
+ * @public
+ */
+ instancesLatest: function() {
+ return this.__instancesLatestArr;
+ },
+
+ /**
+ * For public use only, not to be used by plugins (use ::_off() instead)
+ *
+ * @return {core}
+ * @public
+ */
+ off: function() {
+ this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For public use only, not to be used by plugins (use ::_on() instead)
+ *
+ * @return {core}
+ * @public
+ */
+ on: function() {
+ this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For public use only, not to be used by plugins (use ::_one() instead)
+ *
+ * @return {core}
+ * @public
+ */
+ one: function() {
+ this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * Returns all HTML elements which have one or more tooltips
+ *
+ * @param {string} selector optional Use this to restrict the results
+ * to the descendants of an element
+ * @return {array} An array of HTML elements
+ * @public
+ */
+ origins: function(selector) {
+
+ var sel = selector ?
+ selector +' ' :
+ '';
+
+ return $(sel +'.tooltipstered').toArray();
+ },
+
+ /**
+ * Change default options for all future instances
+ *
+ * @param {object} d The options that should be made defaults
+ * @return {core}
+ * @public
+ */
+ setDefaults: function(d) {
+ $.extend(defaults, d);
+ return this;
+ },
+
+ /**
+ * For users to trigger their handlers on the public emitter
+ *
+ * @returns {core}
+ * @public
+ */
+ triggerHandler: function() {
+ this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ return this;
+ }
+};
+
+// $.tooltipster will be used to call core methods
+$.tooltipster = new core();
+
+// the Tooltipster instance class (mind the capital T)
+$.Tooltipster = function(element, options) {
+
+ // list of instance variables
+
+ // stack of custom callbacks provided as parameters to API methods
+ this.__callbacks = {
+ close: [],
+ open: []
+ };
+ // the schedule time of DOM removal
+ this.__closingTime;
+ // this will be the user content shown in the tooltip. A capital "C" is used
+ // because there is also a method called content()
+ this.__Content;
+ // for the size tracker
+ this.__contentBcr;
+ // to disable the tooltip once the destruction has begun
+ this.__destroyed = false;
+ this.__destroying = false;
+ // we can't emit directly on the instance because if a method with the same
+ // name as the event exists, it will be called by jQuery. Se we use a plain
+ // object as emitter. This emitter is for internal use by plugins,
+ // if needed.
+ this.__$emitterPrivate = $({});
+ // this emitter is for the user to listen to events without risking to mess
+ // with our internal listeners
+ this.__$emitterPublic = $({});
+ this.__enabled = true;
+ // the reference to the gc interval
+ this.__garbageCollector;
+ // various position and size data recomputed before each repositioning
+ this.__Geometry;
+ // the tooltip position, saved after each repositioning by a plugin
+ this.__lastPosition;
+ // a unique namespace per instance
+ this.__namespace = 'tooltipster-'+ Math.round(Math.random()*1000000);
+ this.__options;
+ // will be used to support origins in scrollable areas
+ this.__$originParents;
+ this.__pointerIsOverOrigin = false;
+ // to remove themes if needed
+ this.__previousThemes = [];
+ // the state can be either: appearing, stable, disappearing, closed
+ this.__state = 'closed';
+ // timeout references
+ this.__timeouts = {
+ close: [],
+ open: null
+ };
+ // store touch events to be able to detect emulated mouse events
+ this.__touchEvents = [];
+ // the reference to the tracker interval
+ this.__tracker = null;
+ // the element to which this tooltip is associated
+ this._$origin;
+ // this will be the tooltip element (jQuery wrapped HTML element).
+ // It's the job of a plugin to create it and append it to the DOM
+ this._$tooltip;
+
+ // launch
+ this.__init(element, options);
+};
+
+$.Tooltipster.prototype = {
+
+ /**
+ * @param origin
+ * @param options
+ * @private
+ */
+ __init: function(origin, options) {
+
+ var self = this;
+
+ self._$origin = $(origin);
+ self.__options = $.extend(true, {}, defaults, options);
+
+ // some options may need to be reformatted
+ self.__optionsFormat();
+
+ // don't run on old IE if asked no to
+ if ( !env.IE
+ || env.IE >= self.__options.IEmin
+ ) {
+
+ // note: the content is null (empty) by default and can stay that
+ // way if the plugin remains initialized but not fed any content. The
+ // tooltip will just not appear.
+
+ // let's save the initial value of the title attribute for later
+ // restoration if need be.
+ var initialTitle = null;
+
+ // it will already have been saved in case of multiple tooltips
+ if (self._$origin.data('tooltipster-initialTitle') === undefined) {
+
+ initialTitle = self._$origin.attr('title');
+
+ // we do not want initialTitle to be "undefined" because
+ // of how jQuery's .data() method works
+ if (initialTitle === undefined) initialTitle = null;
+
+ self._$origin.data('tooltipster-initialTitle', initialTitle);
+ }
+
+ // If content is provided in the options, it has precedence over the
+ // title attribute.
+ // Note: an empty string is considered content, only 'null' represents
+ // the absence of content.
+ // Also, an existing title="" attribute will result in an empty string
+ // content
+ if (self.__options.content !== null) {
+ self.__contentSet(self.__options.content);
+ }
+ else {
+
+ var selector = self._$origin.attr('data-tooltip-content'),
+ $el;
+
+ if (selector){
+ $el = $(selector);
+ }
+
+ if ($el && $el[0]) {
+ self.__contentSet($el.first());
+ }
+ else {
+ self.__contentSet(initialTitle);
+ }
+ }
+
+ self._$origin
+ // strip the title off of the element to prevent the default tooltips
+ // from popping up
+ .removeAttr('title')
+ // to be able to find all instances on the page later (upon window
+ // events in particular)
+ .addClass('tooltipstered');
+
+ // set listeners on the origin
+ self.__prepareOrigin();
+
+ // set the garbage collector
+ self.__prepareGC();
+
+ // init plugins
+ $.each(self.__options.plugins, function(i, pluginName) {
+ self._plug(pluginName);
+ });
+
+ // to detect swiping
+ if (env.hasTouchCapability) {
+ $('body').on('touchmove.'+ self.__namespace +'-triggerOpen', function(event) {
+ self._touchRecordEvent(event);
+ });
+ }
+
+ self
+ // prepare the tooltip when it gets created. This event must
+ // be fired by a plugin
+ ._on('created', function() {
+ self.__prepareTooltip();
+ })
+ // save position information when it's sent by a plugin
+ ._on('repositioned', function(e) {
+ self.__lastPosition = e.position;
+ });
+ }
+ else {
+ self.__options.disabled = true;
+ }
+ },
+
+ /**
+ * Insert the content into the appropriate HTML element of the tooltip
+ *
+ * @returns {self}
+ * @private
+ */
+ __contentInsert: function() {
+
+ var self = this,
+ $el = self._$tooltip.find('.tooltipster-content'),
+ formattedContent = self.__Content,
+ format = function(content) {
+ formattedContent = content;
+ };
+
+ self._trigger({
+ type: 'format',
+ content: self.__Content,
+ format: format
+ });
+
+ if (self.__options.functionFormat) {
+
+ formattedContent = self.__options.functionFormat.call(
+ self,
+ self,
+ { origin: self._$origin[0] },
+ self.__Content
+ );
+ }
+
+ if (typeof formattedContent === 'string' && !self.__options.contentAsHTML) {
+ $el.text(formattedContent);
+ }
+ else {
+ $el
+ .empty()
+ .append(formattedContent);
+ }
+
+ return self;
+ },
+
+ /**
+ * Save the content, cloning it beforehand if need be
+ *
+ * @param content
+ * @returns {self}
+ * @private
+ */
+ __contentSet: function(content) {
+
+ // clone if asked. Cloning the object makes sure that each instance has its
+ // own version of the content (in case a same object were provided for several
+ // instances)
+ // reminder: typeof null === object
+ if (content instanceof $ && this.__options.contentCloning) {
+ content = content.clone(true);
+ }
+
+ this.__Content = content;
+
+ this._trigger({
+ type: 'updated',
+ content: content
+ });
+
+ return this;
+ },
+
+ /**
+ * Error message about a method call made after destruction
+ *
+ * @private
+ */
+ __destroyError: function() {
+ throw new Error('This tooltip has been destroyed and cannot execute your method call.');
+ },
+
+ /**
+ * Gather all information about dimensions and available space,
+ * called before every repositioning
+ *
+ * @private
+ * @returns {object}
+ */
+ __geometry: function() {
+
+ var self = this,
+ $target = self._$origin,
+ originIsArea = self._$origin.is('area');
+
+ // if this._$origin is a map area, the target we'll need
+ // the dimensions of is actually the image using the map,
+ // not the area itself
+ if (originIsArea) {
+
+ var mapName = self._$origin.parent().attr('name');
+
+ $target = $('img[usemap="#'+ mapName +'"]');
+ }
+
+ var bcr = $target[0].getBoundingClientRect(),
+ $document = $(env.window.document),
+ $window = $(env.window),
+ $parent = $target,
+ // some useful properties of important elements
+ geo = {
+ // available space for the tooltip, see down below
+ available: {
+ document: null,
+ window: null
+ },
+ document: {
+ size: {
+ height: $document.height(),
+ width: $document.width()
+ }
+ },
+ window: {
+ scroll: {
+ // the second ones are for IE compatibility
+ left: env.window.scrollX || env.window.document.documentElement.scrollLeft,
+ top: env.window.scrollY || env.window.document.documentElement.scrollTop
+ },
+ size: {
+ height: $window.height(),
+ width: $window.width()
+ }
+ },
+ origin: {
+ // the origin has a fixed lineage if itself or one of its
+ // ancestors has a fixed position
+ fixedLineage: false,
+ // relative to the document
+ offset: {},
+ size: {
+ height: bcr.bottom - bcr.top,
+ width: bcr.right - bcr.left
+ },
+ usemapImage: originIsArea ? $target[0] : null,
+ // relative to the window
+ windowOffset: {
+ bottom: bcr.bottom,
+ left: bcr.left,
+ right: bcr.right,
+ top: bcr.top
+ }
+ }
+ },
+ geoFixed = false;
+
+ // if the element is a map area, some properties may need
+ // to be recalculated
+ if (originIsArea) {
+
+ var shape = self._$origin.attr('shape'),
+ coords = self._$origin.attr('coords');
+
+ if (coords) {
+
+ coords = coords.split(',');
+
+ $.map(coords, function(val, i) {
+ coords[i] = parseInt(val);
+ });
+ }
+
+ // if the image itself is the area, nothing more to do
+ if (shape != 'default') {
+
+ switch(shape) {
+
+ case 'circle':
+
+ var circleCenterLeft = coords[0],
+ circleCenterTop = coords[1],
+ circleRadius = coords[2],
+ areaTopOffset = circleCenterTop - circleRadius,
+ areaLeftOffset = circleCenterLeft - circleRadius;
+
+ geo.origin.size.height = circleRadius * 2;
+ geo.origin.size.width = geo.origin.size.height;
+
+ geo.origin.windowOffset.left += areaLeftOffset;
+ geo.origin.windowOffset.top += areaTopOffset;
+
+ break;
+
+ case 'rect':
+
+ var areaLeft = coords[0],
+ areaTop = coords[1],
+ areaRight = coords[2],
+ areaBottom = coords[3];
+
+ geo.origin.size.height = areaBottom - areaTop;
+ geo.origin.size.width = areaRight - areaLeft;
+
+ geo.origin.windowOffset.left += areaLeft;
+ geo.origin.windowOffset.top += areaTop;
+
+ break;
+
+ case 'poly':
+
+ var areaSmallestX = 0,
+ areaSmallestY = 0,
+ areaGreatestX = 0,
+ areaGreatestY = 0,
+ arrayAlternate = 'even';
+
+ for (var i = 0; i < coords.length; i++) {
+
+ var areaNumber = coords[i];
+
+ if (arrayAlternate == 'even') {
+
+ if (areaNumber > areaGreatestX) {
+
+ areaGreatestX = areaNumber;
+
+ if (i === 0) {
+ areaSmallestX = areaGreatestX;
+ }
+ }
+
+ if (areaNumber < areaSmallestX) {
+ areaSmallestX = areaNumber;
+ }
+
+ arrayAlternate = 'odd';
+ }
+ else {
+ if (areaNumber > areaGreatestY) {
+
+ areaGreatestY = areaNumber;
+
+ if (i == 1) {
+ areaSmallestY = areaGreatestY;
+ }
+ }
+
+ if (areaNumber < areaSmallestY) {
+ areaSmallestY = areaNumber;
+ }
+
+ arrayAlternate = 'even';
+ }
+ }
+
+ geo.origin.size.height = areaGreatestY - areaSmallestY;
+ geo.origin.size.width = areaGreatestX - areaSmallestX;
+
+ geo.origin.windowOffset.left += areaSmallestX;
+ geo.origin.windowOffset.top += areaSmallestY;
+
+ break;
+ }
+ }
+ }
+
+ // user callback through an event
+ var edit = function(r) {
+ geo.origin.size.height = r.height,
+ geo.origin.windowOffset.left = r.left,
+ geo.origin.windowOffset.top = r.top,
+ geo.origin.size.width = r.width
+ };
+
+ self._trigger({
+ type: 'geometry',
+ edit: edit,
+ geometry: {
+ height: geo.origin.size.height,
+ left: geo.origin.windowOffset.left,
+ top: geo.origin.windowOffset.top,
+ width: geo.origin.size.width
+ }
+ });
+
+ // calculate the remaining properties with what we got
+
+ geo.origin.windowOffset.right = geo.origin.windowOffset.left + geo.origin.size.width;
+ geo.origin.windowOffset.bottom = geo.origin.windowOffset.top + geo.origin.size.height;
+
+ geo.origin.offset.left = geo.origin.windowOffset.left + env.window.scrollX;
+ geo.origin.offset.top = geo.origin.windowOffset.top + env.window.scrollY;
+ geo.origin.offset.bottom = geo.origin.offset.top + geo.origin.size.height;
+ geo.origin.offset.right = geo.origin.offset.left + geo.origin.size.width;
+
+ // the space that is available to display the tooltip relatively to the document
+ geo.available.document = {
+ bottom: {
+ height: geo.document.size.height - geo.origin.offset.bottom,
+ width: geo.document.size.width
+ },
+ left: {
+ height: geo.document.size.height,
+ width: geo.origin.offset.left
+ },
+ right: {
+ height: geo.document.size.height,
+ width: geo.document.size.width - geo.origin.offset.right
+ },
+ top: {
+ height: geo.origin.offset.top,
+ width: geo.document.size.width
+ }
+ };
+
+ // the space that is available to display the tooltip relatively to the viewport
+ // (the resulting values may be negative if the origin overflows the viewport)
+ geo.available.window = {
+ bottom: {
+ // the inner max is here to make sure the available height is no bigger
+ // than the viewport height (when the origin is off screen at the top).
+ // The outer max just makes sure that the height is not negative (when
+ // the origin overflows at the bottom).
+ height: Math.max(geo.window.size.height - Math.max(geo.origin.windowOffset.bottom, 0), 0),
+ width: geo.window.size.width
+ },
+ left: {
+ height: geo.window.size.height,
+ width: Math.max(geo.origin.windowOffset.left, 0)
+ },
+ right: {
+ height: geo.window.size.height,
+ width: Math.max(geo.window.size.width - Math.max(geo.origin.windowOffset.right, 0), 0)
+ },
+ top: {
+ height: Math.max(geo.origin.windowOffset.top, 0),
+ width: geo.window.size.width
+ }
+ };
+
+ while ($parent[0].tagName.toLowerCase() != 'html') {
+
+ if ($parent.css('position') == 'fixed') {
+ geo.origin.fixedLineage = true;
+ break;
+ }
+
+ $parent = $parent.parent();
+ }
+
+ return geo;
+ },
+
+ /**
+ * Some options may need to be formated before being used
+ *
+ * @returns {self}
+ * @private
+ */
+ __optionsFormat: function() {
+
+ if (typeof this.__options.animationDuration == 'number') {
+ this.__options.animationDuration = [this.__options.animationDuration, this.__options.animationDuration];
+ }
+
+ if (typeof this.__options.delay == 'number') {
+ this.__options.delay = [this.__options.delay, this.__options.delay];
+ }
+
+ if (typeof this.__options.delayTouch == 'number') {
+ this.__options.delayTouch = [this.__options.delayTouch, this.__options.delayTouch];
+ }
+
+ if (typeof this.__options.theme == 'string') {
+ this.__options.theme = [this.__options.theme];
+ }
+
+ // determine the future parent
+ if (typeof this.__options.parent == 'string') {
+ this.__options.parent = $(this.__options.parent);
+ }
+
+ if (this.__options.trigger == 'hover') {
+
+ this.__options.triggerOpen = {
+ mouseenter: true,
+ touchstart: true
+ };
+
+ this.__options.triggerClose = {
+ mouseleave: true,
+ originClick: true,
+ touchleave: true
+ };
+ }
+ else if (this.__options.trigger == 'click') {
+
+ this.__options.triggerOpen = {
+ click: true,
+ tap: true
+ };
+
+ this.__options.triggerClose = {
+ click: true,
+ tap: true
+ };
+ }
+
+ // for the plugins
+ this._trigger('options');
+
+ return this;
+ },
+
+ /**
+ * Schedules or cancels the garbage collector task
+ *
+ * @returns {self}
+ * @private
+ */
+ __prepareGC: function() {
+
+ var self = this;
+
+ // in case the selfDestruction option has been changed by a method call
+ if (self.__options.selfDestruction) {
+
+ // the GC task
+ self.__garbageCollector = setInterval(function() {
+
+ var now = new Date().getTime();
+
+ // forget the old events
+ self.__touchEvents = $.grep(self.__touchEvents, function(event, i) {
+ // 1 minute
+ return now - event.time > 60000;
+ });
+
+ // auto-destruct if the origin is gone
+ if (!bodyContains(self._$origin)) {
+ self.destroy();
+ }
+ }, 20000);
+ }
+ else {
+ clearInterval(self.__garbageCollector);
+ }
+
+ return self;
+ },
+
+ /**
+ * Sets listeners on the origin if the open triggers require them.
+ * Unlike the listeners set at opening time, these ones
+ * remain even when the tooltip is closed. It has been made a
+ * separate method so it can be called when the triggers are
+ * changed in the options. Closing is handled in _open()
+ * because of the bindings that may be needed on the tooltip
+ * itself
+ *
+ * @returns {self}
+ * @private
+ */
+ __prepareOrigin: function() {
+
+ var self = this;
+
+ // in case we're resetting the triggers
+ self._$origin.off('.'+ self.__namespace +'-triggerOpen');
+
+ // if the device is touch capable, even if only mouse triggers
+ // are asked, we need to listen to touch events to know if the mouse
+ // events are actually emulated (so we can ignore them)
+ if (env.hasTouchCapability) {
+
+ self._$origin.on(
+ 'touchstart.'+ self.__namespace +'-triggerOpen ' +
+ 'touchend.'+ self.__namespace +'-triggerOpen ' +
+ 'touchcancel.'+ self.__namespace +'-triggerOpen',
+ function(event){
+ self._touchRecordEvent(event);
+ }
+ );
+ }
+
+ // mouse click and touch tap work the same way
+ if ( self.__options.triggerOpen.click
+ || (self.__options.triggerOpen.tap && env.hasTouchCapability)
+ ) {
+
+ var eventNames = '';
+ if (self.__options.triggerOpen.click) {
+ eventNames += 'click.'+ self.__namespace +'-triggerOpen ';
+ }
+ if (self.__options.triggerOpen.tap && env.hasTouchCapability) {
+ eventNames += 'touchend.'+ self.__namespace +'-triggerOpen';
+ }
+
+ self._$origin.on(eventNames, function(event) {
+ if (self._touchIsMeaningfulEvent(event)) {
+ self._open(event);
+ }
+ });
+ }
+
+ // mouseenter and touch start work the same way
+ if ( self.__options.triggerOpen.mouseenter
+ || (self.__options.triggerOpen.touchstart && env.hasTouchCapability)
+ ) {
+
+ var eventNames = '';
+ if (self.__options.triggerOpen.mouseenter) {
+ eventNames += 'mouseenter.'+ self.__namespace +'-triggerOpen ';
+ }
+ if (self.__options.triggerOpen.touchstart && env.hasTouchCapability) {
+ eventNames += 'touchstart.'+ self.__namespace +'-triggerOpen';
+ }
+
+ self._$origin.on(eventNames, function(event) {
+ if ( self._touchIsTouchEvent(event)
+ || !self._touchIsEmulatedEvent(event)
+ ) {
+ self.__pointerIsOverOrigin = true;
+ self._openShortly(event);
+ }
+ });
+ }
+
+ // info for the mouseleave/touchleave close triggers when they use a delay
+ if ( self.__options.triggerClose.mouseleave
+ || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
+ ) {
+
+ var eventNames = '';
+ if (self.__options.triggerClose.mouseleave) {
+ eventNames += 'mouseleave.'+ self.__namespace +'-triggerOpen ';
+ }
+ if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
+ eventNames += 'touchend.'+ self.__namespace +'-triggerOpen touchcancel.'+ self.__namespace +'-triggerOpen';
+ }
+
+ self._$origin.on(eventNames, function(event) {
+
+ if (self._touchIsMeaningfulEvent(event)) {
+ self.__pointerIsOverOrigin = false;
+ }
+ });
+ }
+
+ return self;
+ },
+
+ /**
+ * Do the things that need to be done only once after the tooltip
+ * HTML element it has been created. It has been made a separate
+ * method so it can be called when options are changed. Remember
+ * that the tooltip may actually exist in the DOM before it is
+ * opened, and present after it has been closed: it's the display
+ * plugin that takes care of handling it.
+ *
+ * @returns {self}
+ * @private
+ */
+ __prepareTooltip: function() {
+
+ var self = this,
+ p = self.__options.interactive ? 'auto' : '';
+
+ // this will be useful to know quickly if the tooltip is in
+ // the DOM or not
+ self._$tooltip
+ .attr('id', self.__namespace)
+ .css({
+ // pointer events
+ 'pointer-events': p,
+ zIndex: self.__options.zIndex
+ });
+
+ // themes
+ // remove the old ones and add the new ones
+ $.each(self.__previousThemes, function(i, theme) {
+ self._$tooltip.removeClass(theme);
+ });
+ $.each(self.__options.theme, function(i, theme) {
+ self._$tooltip.addClass(theme);
+ });
+
+ self.__previousThemes = $.merge([], self.__options.theme);
+
+ return self;
+ },
+
+ /**
+ * Handles the scroll on any of the parents of the origin (when the
+ * tooltip is open)
+ *
+ * @param {object} event
+ * @returns {self}
+ * @private
+ */
+ __scrollHandler: function(event) {
+
+ var self = this;
+
+ if (self.__options.triggerClose.scroll) {
+ self._close(event);
+ }
+ else {
+
+ // if the scroll happened on the window
+ if (event.target === env.window.document) {
+
+ // if the origin has a fixed lineage, window scroll will have no
+ // effect on its position nor on the position of the tooltip
+ if (!self.__Geometry.origin.fixedLineage) {
+
+ // we don't need to do anything unless repositionOnScroll is true
+ // because the tooltip will already have moved with the window
+ // (and of course with the origin)
+ if (self.__options.repositionOnScroll) {
+ self.reposition(event);
+ }
+ }
+ }
+ // if the scroll happened on another parent of the tooltip, it means
+ // that it's in a scrollable area and now needs to have its position
+ // adjusted or recomputed, depending ont the repositionOnScroll
+ // option. Also, if the origin is partly hidden due to a parent that
+ // hides its overflow, we'll just hide (not close) the tooltip.
+ else {
+
+ var g = self.__geometry(),
+ overflows = false;
+
+ // a fixed position origin is not affected by the overflow hiding
+ // of a parent
+ if (self._$origin.css('position') != 'fixed') {
+
+ self.__$originParents.each(function(i, el) {
+
+ var $el = $(el),
+ overflowX = $el.css('overflow-x'),
+ overflowY = $el.css('overflow-y');
+
+ if (overflowX != 'visible' || overflowY != 'visible') {
+
+ var bcr = el.getBoundingClientRect();
+
+ if (overflowX != 'visible') {
+
+ if ( g.origin.windowOffset.left < bcr.left
+ || g.origin.windowOffset.right > bcr.right
+ ) {
+ overflows = true;
+ return false;
+ }
+ }
+
+ if (overflowY != 'visible') {
+
+ if ( g.origin.windowOffset.top < bcr.top
+ || g.origin.windowOffset.bottom > bcr.bottom
+ ) {
+ overflows = true;
+ return false;
+ }
+ }
+ }
+
+ // no need to go further if fixed, for the same reason as above
+ if ($el.css('position') == 'fixed') {
+ return false;
+ }
+ });
+ }
+
+ if (overflows) {
+ self._$tooltip.css('visibility', 'hidden');
+ }
+ else {
+ self._$tooltip.css('visibility', 'visible');
+
+ // reposition
+ if (self.__options.repositionOnScroll) {
+ self.reposition(event);
+ }
+ // or just adjust offset
+ else {
+
+ // we have to use offset and not windowOffset because this way,
+ // only the scroll distance of the scrollable areas are taken into
+ // account (the scrolltop value of the main window must be
+ // ignored since the tooltip already moves with it)
+ var offsetLeft = g.origin.offset.left - self.__Geometry.origin.offset.left,
+ offsetTop = g.origin.offset.top - self.__Geometry.origin.offset.top;
+
+ // add the offset to the position initially computed by the display plugin
+ self._$tooltip.css({
+ left: self.__lastPosition.coord.left + offsetLeft,
+ top: self.__lastPosition.coord.top + offsetTop
+ });
+ }
+ }
+ }
+
+ self._trigger({
+ type: 'scroll',
+ event: event
+ });
+ }
+
+ return self;
+ },
+
+ /**
+ * Changes the state of the tooltip
+ *
+ * @param {string} state
+ * @returns {self}
+ * @private
+ */
+ __stateSet: function(state) {
+
+ this.__state = state;
+
+ this._trigger({
+ type: 'state',
+ state: state
+ });
+
+ return this;
+ },
+
+ /**
+ * Clear appearance timeouts
+ *
+ * @returns {self}
+ * @private
+ */
+ __timeoutsClear: function() {
+
+ // there is only one possible open timeout: the delayed opening
+ // when the mouseenter/touchstart open triggers are used
+ clearTimeout(this.__timeouts.open);
+ this.__timeouts.open = null;
+
+ // ... but several close timeouts: the delayed closing when the
+ // mouseleave close trigger is used and the timer option
+ $.each(this.__timeouts.close, function(i, timeout) {
+ clearTimeout(timeout);
+ });
+ this.__timeouts.close = [];
+
+ return this;
+ },
+
+ /**
+ * Start the tracker that will make checks at regular intervals
+ *
+ * @returns {self}
+ * @private
+ */
+ __trackerStart: function() {
+
+ var self = this,
+ $content = self._$tooltip.find('.tooltipster-content');
+
+ // get the initial content size
+ if (self.__options.trackTooltip) {
+ self.__contentBcr = $content[0].getBoundingClientRect();
+ }
+
+ self.__tracker = setInterval(function() {
+
+ // if the origin or tooltip elements have been removed.
+ // Note: we could destroy the instance now if the origin has
+ // been removed but we'll leave that task to our garbage collector
+ if (!bodyContains(self._$origin) || !bodyContains(self._$tooltip)) {
+ self._close();
+ }
+ // if everything is alright
+ else {
+
+ // compare the former and current positions of the origin to reposition
+ // the tooltip if need be
+ if (self.__options.trackOrigin) {
+
+ var g = self.__geometry(),
+ identical = false;
+
+ // compare size first (a change requires repositioning too)
+ if (areEqual(g.origin.size, self.__Geometry.origin.size)) {
+
+ // for elements that have a fixed lineage (see __geometry()), we track the
+ // top and left properties (relative to window)
+ if (self.__Geometry.origin.fixedLineage) {
+ if (areEqual(g.origin.windowOffset, self.__Geometry.origin.windowOffset)) {
+ identical = true;
+ }
+ }
+ // otherwise, track total offset (relative to document)
+ else {
+ if (areEqual(g.origin.offset, self.__Geometry.origin.offset)) {
+ identical = true;
+ }
+ }
+ }
+
+ if (!identical) {
+
+ // close the tooltip when using the mouseleave close trigger
+ // (see https://github.com/iamceege/tooltipster/pull/253)
+ if (self.__options.triggerClose.mouseleave) {
+ self._close();
+ }
+ else {
+ self.reposition();
+ }
+ }
+ }
+
+ if (self.__options.trackTooltip) {
+
+ var currentBcr = $content[0].getBoundingClientRect();
+
+ if ( currentBcr.height !== self.__contentBcr.height
+ || currentBcr.width !== self.__contentBcr.width
+ ) {
+ self.reposition();
+ self.__contentBcr = currentBcr;
+ }
+ }
+ }
+ }, self.__options.trackerInterval);
+
+ return self;
+ },
+
+ /**
+ * Closes the tooltip (after the closing delay)
+ *
+ * @param event
+ * @param callback
+ * @returns {self}
+ * @protected
+ */
+ _close: function(event, callback) {
+
+ var self = this,
+ ok = true;
+
+ self._trigger({
+ type: 'close',
+ event: event,
+ stop: function() {
+ ok = false;
+ }
+ });
+
+ // a destroying tooltip may not refuse to close
+ if (ok || self.__destroying) {
+
+ // save the method custom callback and cancel any open method custom callbacks
+ if (callback) self.__callbacks.close.push(callback);
+ self.__callbacks.open = [];
+
+ // clear open/close timeouts
+ self.__timeoutsClear();
+
+ var finishCallbacks = function() {
+
+ // trigger any close method custom callbacks and reset them
+ $.each(self.__callbacks.close, function(i,c) {
+ c.call(self, self, {
+ event: event,
+ origin: self._$origin[0]
+ });
+ });
+
+ self.__callbacks.close = [];
+ };
+
+ if (self.__state != 'closed') {
+
+ var necessary = true,
+ d = new Date(),
+ now = d.getTime(),
+ newClosingTime = now + self.__options.animationDuration[1];
+
+ // the tooltip may already already be disappearing, but if a new
+ // call to close() is made after the animationDuration was changed
+ // to 0 (for example), we ought to actually close it sooner than
+ // previously scheduled. In that case it should be noted that the
+ // browser will not adapt the animation duration to the new
+ // animationDuration that was set after the start of the closing
+ // animation.
+ // Note: the same thing could be considered at opening, but is not
+ // really useful since the tooltip is actually opened immediately
+ // upon a call to _open(). Since it would not make the opening
+ // animation finish sooner, its sole impact would be to trigger the
+ // state event and the open callbacks sooner than the actual end of
+ // the opening animation, which is not great.
+ if (self.__state == 'disappearing') {
+
+ if (newClosingTime > self.__closingTime) {
+ necessary = false;
+ }
+ }
+
+ if (necessary) {
+
+ self.__closingTime = newClosingTime;
+
+ if (self.__state != 'disappearing') {
+ self.__stateSet('disappearing');
+ }
+
+ var finish = function() {
+
+ // stop the tracker
+ clearInterval(self.__tracker);
+
+ // a "beforeClose" option has been asked several times but would
+ // probably useless since the content element is still accessible
+ // via ::content(), and because people can always use listeners
+ // inside their content to track what's going on. For the sake of
+ // simplicity, this has been denied. Bur for the rare people who
+ // really need the option (for old browsers or for the case where
+ // detaching the content is actually destructive, for file or
+ // password inputs for example), this event will do the work.
+ self._trigger({
+ type: 'closing',
+ event: event
+ });
+
+ // unbind listeners which are no longer needed
+
+ self._$tooltip
+ .off('.'+ self.__namespace +'-triggerClose')
+ .removeClass('tooltipster-dying');
+
+ // orientationchange, scroll and resize listeners
+ $(env.window).off('.'+ self.__namespace +'-triggerClose');
+
+ // scroll listeners
+ self.__$originParents.each(function(i, el) {
+ $(el).off('scroll.'+ self.__namespace +'-triggerClose');
+ });
+ // clear the array to prevent memory leaks
+ self.__$originParents = null;
+
+ $('body').off('.'+ self.__namespace +'-triggerClose');
+
+ self._$origin.off('.'+ self.__namespace +'-triggerClose');
+
+ self._off('dismissable');
+
+ // a plugin that would like to remove the tooltip from the
+ // DOM when closed should bind on this
+ self.__stateSet('closed');
+
+ // trigger event
+ self._trigger({
+ type: 'after',
+ event: event
+ });
+
+ // call our constructor custom callback function
+ if (self.__options.functionAfter) {
+ self.__options.functionAfter.call(self, self, {
+ event: event
+ });
+ }
+
+ // call our method custom callbacks functions
+ finishCallbacks();
+ };
+
+ if (env.hasTransitions) {
+
+ self._$tooltip.css({
+ '-moz-animation-duration': self.__options.animationDuration[1] + 'ms',
+ '-ms-animation-duration': self.__options.animationDuration[1] + 'ms',
+ '-o-animation-duration': self.__options.animationDuration[1] + 'ms',
+ '-webkit-animation-duration': self.__options.animationDuration[1] + 'ms',
+ 'animation-duration': self.__options.animationDuration[1] + 'ms',
+ 'transition-duration': self.__options.animationDuration[1] + 'ms'
+ });
+
+ self._$tooltip
+ // clear both potential open and close tasks
+ .clearQueue()
+ .removeClass('tooltipster-show')
+ // for transitions only
+ .addClass('tooltipster-dying');
+
+ if (self.__options.animationDuration[1] > 0) {
+ self._$tooltip.delay(self.__options.animationDuration[1]);
+ }
+
+ self._$tooltip.queue(finish);
+ }
+ else {
+
+ self._$tooltip
+ .stop()
+ .fadeOut(self.__options.animationDuration[1], finish);
+ }
+ }
+ }
+ // if the tooltip is already closed, we still need to trigger
+ // the method custom callbacks
+ else {
+ finishCallbacks();
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @returns {self}
+ * @protected
+ */
+ _off: function() {
+ this.__$emitterPrivate.off.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @returns {self}
+ * @protected
+ */
+ _on: function() {
+ this.__$emitterPrivate.on.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * For internal use by plugins, if needed
+ *
+ * @returns {self}
+ * @protected
+ */
+ _one: function() {
+ this.__$emitterPrivate.one.apply(this.__$emitterPrivate, Array.prototype.slice.apply(arguments));
+ return this;
+ },
+
+ /**
+ * Opens the tooltip right away
+ *
+ * @param event
+ * @param callback
+ * @returns {self}
+ * @protected
+ */
+ _open: function(event, callback) {
+
+ var self = this;
+
+ // if the destruction process has not begun and if this was not
+ // triggered by an unwanted emulated click event
+ if (!self.__destroying) {
+
+ // check that the origin is still in the DOM
+ if ( bodyContains(self._$origin)
+ // if the tooltip is enabled
+ && self.__enabled
+ ) {
+
+ var ok = true;
+
+ // if the tooltip is not open yet, we need to call functionBefore.
+ // otherwise we can jst go on
+ if (self.__state == 'closed') {
+
+ // trigger an event. The event.stop function allows the callback
+ // to prevent the opening of the tooltip
+ self._trigger({
+ type: 'before',
+ event: event,
+ stop: function() {
+ ok = false;
+ }
+ });
+
+ if (ok && self.__options.functionBefore) {
+
+ // call our custom function before continuing
+ ok = self.__options.functionBefore.call(self, self, {
+ event: event,
+ origin: self._$origin[0]
+ });
+ }
+ }
+
+ if (ok !== false) {
+
+ // if there is some content
+ if (self.__Content !== null) {
+
+ // save the method callback and cancel close method callbacks
+ if (callback) {
+ self.__callbacks.open.push(callback);
+ }
+ self.__callbacks.close = [];
+
+ // get rid of any appearance timeouts
+ self.__timeoutsClear();
+
+ var extraTime,
+ finish = function() {
+
+ if (self.__state != 'stable') {
+ self.__stateSet('stable');
+ }
+
+ // trigger any open method custom callbacks and reset them
+ $.each(self.__callbacks.open, function(i,c) {
+ c.call(self, self, {
+ origin: self._$origin[0],
+ tooltip: self._$tooltip[0]
+ });
+ });
+
+ self.__callbacks.open = [];
+ };
+
+ // if the tooltip is already open
+ if (self.__state !== 'closed') {
+
+ // the timer (if any) will start (or restart) right now
+ extraTime = 0;
+
+ // if it was disappearing, cancel that
+ if (self.__state === 'disappearing') {
+
+ self.__stateSet('appearing');
+
+ if (env.hasTransitions) {
+
+ self._$tooltip
+ .clearQueue()
+ .removeClass('tooltipster-dying')
+ .addClass('tooltipster-show');
+
+ if (self.__options.animationDuration[0] > 0) {
+ self._$tooltip.delay(self.__options.animationDuration[0]);
+ }
+
+ self._$tooltip.queue(finish);
+ }
+ else {
+ // in case the tooltip was currently fading out, bring it back
+ // to life
+ self._$tooltip
+ .stop()
+ .fadeIn(finish);
+ }
+ }
+ // if the tooltip is already open, we still need to trigger the method
+ // custom callback
+ else if (self.__state == 'stable') {
+ finish();
+ }
+ }
+ // if the tooltip isn't already open, open it
+ else {
+
+ // a plugin must bind on this and store the tooltip in this._$tooltip
+ self.__stateSet('appearing');
+
+ // the timer (if any) will start when the tooltip has fully appeared
+ // after its transition
+ extraTime = self.__options.animationDuration[0];
+
+ // insert the content inside the tooltip
+ self.__contentInsert();
+
+ // reposition the tooltip and attach to the DOM
+ self.reposition(event, true);
+
+ // animate in the tooltip. If the display plugin wants no css
+ // animations, it may override the animation option with a
+ // dummy value that will produce no effect
+ if (env.hasTransitions) {
+
+ // note: there seems to be an issue with start animations which
+ // are randomly not played on fast devices in both Chrome and FF,
+ // couldn't find a way to solve it yet. It seems that applying
+ // the classes before appending to the DOM helps a little, but
+ // it messes up some CSS transitions. The issue almost never
+ // happens when delay[0]==0 though
+ self._$tooltip
+ .addClass('tooltipster-'+ self.__options.animation)
+ .addClass('tooltipster-initial')
+ .css({
+ '-moz-animation-duration': self.__options.animationDuration[0] + 'ms',
+ '-ms-animation-duration': self.__options.animationDuration[0] + 'ms',
+ '-o-animation-duration': self.__options.animationDuration[0] + 'ms',
+ '-webkit-animation-duration': self.__options.animationDuration[0] + 'ms',
+ 'animation-duration': self.__options.animationDuration[0] + 'ms',
+ 'transition-duration': self.__options.animationDuration[0] + 'ms'
+ });
+
+ setTimeout(
+ function() {
+
+ // a quick hover may have already triggered a mouseleave
+ if (self.__state != 'closed') {
+
+ self._$tooltip
+ .addClass('tooltipster-show')
+ .removeClass('tooltipster-initial');
+
+ if (self.__options.animationDuration[0] > 0) {
+ self._$tooltip.delay(self.__options.animationDuration[0]);
+ }
+
+ self._$tooltip.queue(finish);
+ }
+ },
+ 0
+ );
+ }
+ else {
+
+ // old browsers will have to live with this
+ self._$tooltip
+ .css('display', 'none')
+ .fadeIn(self.__options.animationDuration[0], finish);
+ }
+
+ // checks if the origin is removed while the tooltip is open
+ self.__trackerStart();
+
+ // NOTE: the listeners below have a '-triggerClose' namespace
+ // because we'll remove them when the tooltip closes (unlike
+ // the '-triggerOpen' listeners). So some of them are actually
+ // not about close triggers, rather about positioning.
+
+ $(env.window)
+ // reposition on resize
+ .on('resize.'+ self.__namespace +'-triggerClose', function(e) {
+ self.reposition(e);
+ })
+ // same as below for parents
+ .on('scroll.'+ self.__namespace +'-triggerClose', function(e) {
+ self.__scrollHandler(e);
+ });
+
+ self.__$originParents = self._$origin.parents();
+
+ // scrolling may require the tooltip to be moved or even
+ // repositioned in some cases
+ self.__$originParents.each(function(i, parent) {
+
+ $(parent).on('scroll.'+ self.__namespace +'-triggerClose', function(e) {
+ self.__scrollHandler(e);
+ });
+ });
+
+ if ( self.__options.triggerClose.mouseleave
+ || (self.__options.triggerClose.touchleave && env.hasTouchCapability)
+ ) {
+
+ // we use an event to allow users/plugins to control when the mouseleave/touchleave
+ // close triggers will come to action. It allows to have more triggering elements
+ // than just the origin and the tooltip for example, or to cancel/delay the closing,
+ // or to make the tooltip interactive even if it wasn't when it was open, etc.
+ self._on('dismissable', function(event) {
+
+ if (event.dismissable) {
+
+ if (event.delay) {
+
+ timeout = setTimeout(function() {
+ // event.event may be undefined
+ self._close(event.event);
+ }, event.delay);
+
+ self.__timeouts.close.push(timeout);
+ }
+ else {
+ self._close(event);
+ }
+ }
+ else {
+ clearTimeout(timeout);
+ }
+ });
+
+ // now set the listeners that will trigger 'dismissable' events
+ var $elements = self._$origin,
+ eventNamesIn = '',
+ eventNamesOut = '',
+ timeout = null;
+
+ // if we have to allow interaction, bind on the tooltip too
+ if (self.__options.interactive) {
+ $elements = $elements.add(self._$tooltip);
+ }
+
+ if (self.__options.triggerClose.mouseleave) {
+ eventNamesIn += 'mouseenter.'+ self.__namespace +'-triggerClose ';
+ eventNamesOut += 'mouseleave.'+ self.__namespace +'-triggerClose ';
+ }
+ if (self.__options.triggerClose.touchleave && env.hasTouchCapability) {
+ eventNamesIn += 'touchstart.'+ self.__namespace +'-triggerClose';
+ eventNamesOut += 'touchend.'+ self.__namespace +'-triggerClose touchcancel.'+ self.__namespace +'-triggerClose';
+ }
+
+ $elements
+ // close after some time spent outside of the elements
+ .on(eventNamesOut, function(event) {
+
+ // it's ok if the touch gesture ended up to be a swipe,
+ // it's still a "touch leave" situation
+ if ( self._touchIsTouchEvent(event)
+ || !self._touchIsEmulatedEvent(event)
+ ) {
+
+ var delay = (event.type == 'mouseleave') ?
+ self.__options.delay :
+ self.__options.delayTouch;
+
+ self._trigger({
+ delay: delay[1],
+ dismissable: true,
+ event: event,
+ type: 'dismissable'
+ });
+ }
+ })
+ // suspend the mouseleave timeout when the pointer comes back
+ // over the elements
+ .on(eventNamesIn, function(event) {
+
+ // it's also ok if the touch event is a swipe gesture
+ if ( self._touchIsTouchEvent(event)
+ || !self._touchIsEmulatedEvent(event)
+ ) {
+ self._trigger({
+ dismissable: false,
+ event: event,
+ type: 'dismissable'
+ });
+ }
+ });
+ }
+
+ // close the tooltip when the origin gets a mouse click (common behavior of
+ // native tooltips)
+ if (self.__options.triggerClose.originClick) {
+
+ self._$origin.on('click.'+ self.__namespace + '-triggerClose', function(event) {
+
+ // we could actually let a tap trigger this but this feature just
+ // does not make sense on touch devices
+ if ( !self._touchIsTouchEvent(event)
+ && !self._touchIsEmulatedEvent(event)
+ ) {
+ self._close(event);
+ }
+ });
+ }
+
+ // set the same bindings for click and touch on the body to close the tooltip
+ if ( self.__options.triggerClose.click
+ || (self.__options.triggerClose.tap && env.hasTouchCapability)
+ ) {
+
+ // don't set right away since the click/tap event which triggered this method
+ // (if it was a click/tap) is going to bubble up to the body, we don't want it
+ // to close the tooltip immediately after it opened
+ setTimeout(function() {
+
+ if (self.__state != 'closed') {
+
+ var eventNames = '';
+ if (self.__options.triggerClose.click) {
+ eventNames += 'click.'+ self.__namespace +'-triggerClose ';
+ }
+ if (self.__options.triggerClose.tap && env.hasTouchCapability) {
+ eventNames += 'touchend.'+ self.__namespace +'-triggerClose';
+ }
+
+ $('body').on(eventNames, function(event) {
+
+ if (self._touchIsMeaningfulEvent(event)) {
+
+ self._touchRecordEvent(event);
+
+ if (!self.__options.interactive || !$.contains(self._$tooltip[0], event.target)) {
+ self._close(event);
+ }
+ }
+ });
+
+ // needed to detect and ignore swiping
+ if (self.__options.triggerClose.tap && env.hasTouchCapability) {
+
+ $('body').on('touchstart.'+ self.__namespace +'-triggerClose', function(event) {
+ self._touchRecordEvent(event);
+ });
+ }
+ }
+ }, 0);
+ }
+
+ self._trigger('ready');
+
+ // call our custom callback
+ if (self.__options.functionReady) {
+ self.__options.functionReady.call(self, self, {
+ origin: self._$origin[0],
+ tooltip: self._$tooltip[0]
+ });
+ }
+ }
+
+ // if we have a timer set, let the countdown begin
+ if (self.__options.timer > 0) {
+
+ var timeout = setTimeout(function() {
+ self._close();
+ }, self.__options.timer + extraTime);
+
+ self.__timeouts.close.push(timeout);
+ }
+ }
+ }
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * When using the mouseenter/touchstart open triggers, this function will
+ * schedule the opening of the tooltip after the delay, if there is one
+ *
+ * @param event
+ * @returns {self}
+ * @protected
+ */
+ _openShortly: function(event) {
+
+ var self = this,
+ ok = true;
+
+ if (self.__state != 'stable' && self.__state != 'appearing') {
+
+ // if a timeout is not already running
+ if (!self.__timeouts.open) {
+
+ self._trigger({
+ type: 'start',
+ event: event,
+ stop: function() {
+ ok = false;
+ }
+ });
+
+ if (ok) {
+
+ var delay = (event.type.indexOf('touch') == 0) ?
+ self.__options.delayTouch :
+ self.__options.delay;
+
+ if (delay[0]) {
+
+ self.__timeouts.open = setTimeout(function() {
+
+ self.__timeouts.open = null;
+
+ // open only if the pointer (mouse or touch) is still over the origin.
+ // The check on the "meaningful event" can only be made here, after some
+ // time has passed (to know if the touch was a swipe or not)
+ if (self.__pointerIsOverOrigin && self._touchIsMeaningfulEvent(event)) {
+
+ // signal that we go on
+ self._trigger('startend');
+
+ self._open(event);
+ }
+ else {
+ // signal that we cancel
+ self._trigger('startcancel');
+ }
+ }, delay[0]);
+ }
+ else {
+ // signal that we go on
+ self._trigger('startend');
+
+ self._open(event);
+ }
+ }
+ }
+ }
+
+ return self;
+ },
+
+ /**
+ * Meant for plugins to get their options
+ *
+ * @param {string} pluginName The name of the plugin that asks for its options
+ * @param {object} defaultOptions The default options of the plugin
+ * @returns {object} The options
+ * @protected
+ */
+ _optionsExtract: function(pluginName, defaultOptions) {
+
+ var self = this,
+ options = $.extend(true, {}, defaultOptions);
+
+ // if the plugin options were isolated in a property named after the
+ // plugin, use them (prevents conflicts with other plugins)
+ var pluginOptions = self.__options[pluginName];
+
+ // if not, try to get them as regular options
+ if (!pluginOptions){
+
+ pluginOptions = {};
+
+ $.each(defaultOptions, function(optionName, value) {
+
+ var o = self.__options[optionName];
+
+ if (o !== undefined) {
+ pluginOptions[optionName] = o;
+ }
+ });
+ }
+
+ // let's merge the default options and the ones that were provided. We'd want
+ // to do a deep copy but not let jQuery merge arrays, so we'll do a shallow
+ // extend on two levels, that will be enough if options are not more than 1
+ // level deep
+ $.each(options, function(optionName, value) {
+
+ if (pluginOptions[optionName] !== undefined) {
+
+ if (( typeof value == 'object'
+ && !(value instanceof Array)
+ && value != null
+ )
+ &&
+ ( typeof pluginOptions[optionName] == 'object'
+ && !(pluginOptions[optionName] instanceof Array)
+ && pluginOptions[optionName] != null
+ )
+ ) {
+ $.extend(options[optionName], pluginOptions[optionName]);
+ }
+ else {
+ options[optionName] = pluginOptions[optionName];
+ }
+ }
+ });
+
+ return options;
+ },
+
+ /**
+ * Used at instantiation of the plugin, or afterwards by plugins that activate themselves
+ * on existing instances
+ *
+ * @param {object} pluginName
+ * @returns {self}
+ * @protected
+ */
+ _plug: function(pluginName) {
+
+ var plugin = $.tooltipster._plugin(pluginName);
+
+ if (plugin) {
+
+ // if there is a constructor for instances
+ if (plugin.instance) {
+
+ // proxy non-private methods on the instance to allow new instance methods
+ $.tooltipster.__bridge(plugin.instance, this, plugin.name);
+ }
+ }
+ else {
+ throw new Error('The "'+ pluginName +'" plugin is not defined');
+ }
+
+ return this;
+ },
+
+ /**
+ * This will return true if the event is a mouse event which was
+ * emulated by the browser after a touch event. This allows us to
+ * really dissociate mouse and touch triggers.
+ *
+ * There is a margin of error if a real mouse event is fired right
+ * after (within the delay shown below) a touch event on the same
+ * element, but hopefully it should not happen often.
+ *
+ * @returns {boolean}
+ * @protected
+ */
+ _touchIsEmulatedEvent: function(event) {
+
+ var isEmulated = false,
+ now = new Date().getTime();
+
+ for (var i = this.__touchEvents.length - 1; i >= 0; i--) {
+
+ var e = this.__touchEvents[i];
+
+ // delay, in milliseconds. It's supposed to be 300ms in
+ // most browsers (350ms on iOS) to allow a double tap but
+ // can be less (check out FastClick for more info)
+ if (now - e.time < 500) {
+
+ if (e.target === event.target) {
+ isEmulated = true;
+ }
+ }
+ else {
+ break;
+ }
+ }
+
+ return isEmulated;
+ },
+
+ /**
+ * Returns false if the event was an emulated mouse event or
+ * a touch event involved in a swipe gesture.
+ *
+ * @param {object} event
+ * @returns {boolean}
+ * @protected
+ */
+ _touchIsMeaningfulEvent: function(event) {
+ return (
+ (this._touchIsTouchEvent(event) && !this._touchSwiped(event.target))
+ || (!this._touchIsTouchEvent(event) && !this._touchIsEmulatedEvent(event))
+ );
+ },
+
+ /**
+ * Checks if an event is a touch event
+ *
+ * @param {object} event
+ * @returns {boolean}
+ * @protected
+ */
+ _touchIsTouchEvent: function(event){
+ return event.type.indexOf('touch') == 0;
+ },
+
+ /**
+ * Store touch events for a while to detect swiping and emulated mouse events
+ *
+ * @param {object} event
+ * @returns {self}
+ * @protected
+ */
+ _touchRecordEvent: function(event) {
+
+ if (this._touchIsTouchEvent(event)) {
+ event.time = new Date().getTime();
+ this.__touchEvents.push(event);
+ }
+
+ return this;
+ },
+
+ /**
+ * Returns true if a swipe happened after the last touchstart event fired on
+ * event.target.
+ *
+ * We need to differentiate a swipe from a tap before we let the event open
+ * or close the tooltip. A swipe is when a touchmove (scroll) event happens
+ * on the body between the touchstart and the touchend events of an element.
+ *
+ * @param {object} target The HTML element that may have triggered the swipe
+ * @returns {boolean}
+ * @protected
+ */
+ _touchSwiped: function(target) {
+
+ var swiped = false;
+
+ for (var i = this.__touchEvents.length - 1; i >= 0; i--) {
+
+ var e = this.__touchEvents[i];
+
+ if (e.type == 'touchmove') {
+ swiped = true;
+ break;
+ }
+ else if (
+ e.type == 'touchstart'
+ && target === e.target
+ ) {
+ break;
+ }
+ }
+
+ return swiped;
+ },
+
+ /**
+ * Triggers an event on the instance emitters
+ *
+ * @returns {self}
+ * @protected
+ */
+ _trigger: function() {
+
+ var args = Array.prototype.slice.apply(arguments);
+
+ if (typeof args[0] == 'string') {
+ args[0] = { type: args[0] };
+ }
+
+ // add properties to the event
+ args[0].instance = this;
+ args[0].origin = this._$origin ? this._$origin[0] : null;
+ args[0].tooltip = this._$tooltip ? this._$tooltip[0] : null;
+
+ // note: the order of emitters matters
+ this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate, args);
+ $.tooltipster._trigger.apply($.tooltipster, args);
+ this.__$emitterPublic.trigger.apply(this.__$emitterPublic, args);
+
+ return this;
+ },
+
+ /**
+ * Deactivate a plugin on this instance
+ *
+ * @returns {self}
+ * @protected
+ */
+ _unplug: function(pluginName) {
+
+ var self = this;
+
+ // if the plugin has been activated on this instance
+ if (self[pluginName]) {
+
+ var plugin = $.tooltipster._plugin(pluginName);
+
+ // if there is a constructor for instances
+ if (plugin.instance) {
+
+ // unbridge
+ $.each(plugin.instance, function(methodName, fn) {
+
+ // if the method exists (privates methods do not) and comes indeed from
+ // this plugin (may be missing or come from a conflicting plugin).
+ if ( self[methodName]
+ && self[methodName].bridged === self[pluginName]
+ ) {
+ delete self[methodName];
+ }
+ });
+ }
+
+ // destroy the plugin
+ if (self[pluginName].__destroy) {
+ self[pluginName].__destroy();
+ }
+
+ // remove the reference to the plugin instance
+ delete self[pluginName];
+ }
+
+ return self;
+ },
+
+ /**
+ * @see self::_close
+ * @returns {self}
+ * @public
+ */
+ close: function(callback) {
+
+ if (!this.__destroyed) {
+ this._close(null, callback);
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ },
+
+ /**
+ * Sets or gets the content of the tooltip
+ *
+ * @returns {mixed|self}
+ * @public
+ */
+ content: function(content) {
+
+ var self = this;
+
+ // getter method
+ if (content === undefined) {
+ return self.__Content;
+ }
+ // setter method
+ else {
+
+ if (!self.__destroyed) {
+
+ // change the content
+ self.__contentSet(content);
+
+ if (self.__Content !== null) {
+
+ // update the tooltip if it is open
+ if (self.__state !== 'closed') {
+
+ // reset the content in the tooltip
+ self.__contentInsert();
+
+ // reposition and resize the tooltip
+ self.reposition();
+
+ // if we want to play a little animation showing the content changed
+ if (self.__options.updateAnimation) {
+
+ if (env.hasTransitions) {
+
+ // keep the reference in the local scope
+ var animation = self.__options.updateAnimation;
+
+ self._$tooltip.addClass('tooltipster-update-'+ animation);
+
+ // remove the class after a while. The actual duration of the
+ // update animation may be shorter, it's set in the CSS rules
+ setTimeout(function() {
+
+ if (self.__state != 'closed') {
+
+ self._$tooltip.removeClass('tooltipster-update-'+ animation);
+ }
+ }, 1000);
+ }
+ else {
+ self._$tooltip.fadeTo(200, 0.5, function() {
+ if (self.__state != 'closed') {
+ self._$tooltip.fadeTo(200, 1);
+ }
+ });
+ }
+ }
+ }
+ }
+ else {
+ self._close();
+ }
+ }
+ else {
+ self.__destroyError();
+ }
+
+ return self;
+ }
+ },
+
+ /**
+ * Destroys the tooltip
+ *
+ * @returns {self}
+ * @public
+ */
+ destroy: function() {
+
+ var self = this;
+
+ if (!self.__destroyed) {
+
+ if (!self.__destroying) {
+
+ self.__destroying = true;
+
+ self._close(null, function() {
+
+ self._trigger('destroy');
+
+ self.__destroying = false;
+ self.__destroyed = true;
+
+ self._$origin
+ .removeData(self.__namespace)
+ // remove the open trigger listeners
+ .off('.'+ self.__namespace +'-triggerOpen');
+
+ // remove the touch listener
+ $('body').off('.' + self.__namespace +'-triggerOpen');
+
+ var ns = self._$origin.data('tooltipster-ns');
+
+ // if the origin has been removed from DOM, its data may
+ // well have been destroyed in the process and there would
+ // be nothing to clean up or restore
+ if (ns) {
+
+ // if there are no more tooltips on this element
+ if (ns.length === 1) {
+
+ // optional restoration of a title attribute
+ var title = null;
+ if (self.__options.restoration == 'previous') {
+ title = self._$origin.data('tooltipster-initialTitle');
+ }
+ else if (self.__options.restoration == 'current') {
+
+ // old school technique to stringify when outerHTML is not supported
+ title = (typeof self.__Content == 'string') ?
+ self.__Content :
+ $('').append(self.__Content).html();
+ }
+
+ if (title) {
+ self._$origin.attr('title', title);
+ }
+
+ // final cleaning
+
+ self._$origin.removeClass('tooltipstered');
+
+ self._$origin
+ .removeData('tooltipster-ns')
+ .removeData('tooltipster-initialTitle');
+ }
+ else {
+ // remove the instance namespace from the list of namespaces of
+ // tooltips present on the element
+ ns = $.grep(ns, function(el, i) {
+ return el !== self.__namespace;
+ });
+ self._$origin.data('tooltipster-ns', ns);
+ }
+ }
+
+ // last event
+ self._trigger('destroyed');
+
+ // unbind private and public event listeners
+ self._off();
+ self.off();
+
+ // remove external references, just in case
+ self.__Content = null;
+ self.__$emitterPrivate = null;
+ self.__$emitterPublic = null;
+ self.__options.parent = null;
+ self._$origin = null;
+ self._$tooltip = null;
+
+ // make sure the object is no longer referenced in there to prevent
+ // memory leaks
+ $.tooltipster.__instancesLatestArr = $.grep($.tooltipster.__instancesLatestArr, function(el, i) {
+ return self !== el;
+ });
+
+ clearInterval(self.__garbageCollector);
+ });
+ }
+ }
+ else {
+ self.__destroyError();
+ }
+
+ // we return the scope rather than true so that the call to
+ // .tooltipster('destroy') actually returns the matched elements
+ // and applies to all of them
+ return self;
+ },
+
+ /**
+ * Disables the tooltip
+ *
+ * @returns {self}
+ * @public
+ */
+ disable: function() {
+
+ if (!this.__destroyed) {
+
+ // close first, in case the tooltip would not disappear on
+ // its own (no close trigger)
+ this._close();
+ this.__enabled = false;
+
+ return this;
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ },
+
+ /**
+ * Returns the HTML element of the origin
+ *
+ * @returns {self}
+ * @public
+ */
+ elementOrigin: function() {
+
+ if (!this.__destroyed) {
+ return this._$origin[0];
+ }
+ else {
+ this.__destroyError();
+ }
+ },
+
+ /**
+ * Returns the HTML element of the tooltip
+ *
+ * @returns {self}
+ * @public
+ */
+ elementTooltip: function() {
+ return this._$tooltip ? this._$tooltip[0] : null;
+ },
+
+ /**
+ * Enables the tooltip
+ *
+ * @returns {self}
+ * @public
+ */
+ enable: function() {
+ this.__enabled = true;
+ return this;
+ },
+
+ /**
+ * Alias, deprecated in 4.0.0
+ *
+ * @param {function} callback
+ * @returns {self}
+ * @public
+ */
+ hide: function(callback) {
+ return this.close(callback);
+ },
+
+ /**
+ * Returns the instance
+ *
+ * @returns {self}
+ * @public
+ */
+ instance: function() {
+ return this;
+ },
+
+ /**
+ * For public use only, not to be used by plugins (use ::_off() instead)
+ *
+ * @returns {self}
+ * @public
+ */
+ off: function() {
+
+ if (!this.__destroyed) {
+ this.__$emitterPublic.off.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ }
+
+ return this;
+ },
+
+ /**
+ * For public use only, not to be used by plugins (use ::_on() instead)
+ *
+ * @returns {self}
+ * @public
+ */
+ on: function() {
+
+ if (!this.__destroyed) {
+ this.__$emitterPublic.on.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ },
+
+ /**
+ * For public use only, not to be used by plugins
+ *
+ * @returns {self}
+ * @public
+ */
+ one: function() {
+
+ if (!this.__destroyed) {
+ this.__$emitterPublic.one.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ },
+
+ /**
+ * @see self::_open
+ * @returns {self}
+ * @public
+ */
+ open: function(callback) {
+
+ if (!this.__destroyed && !this.__destroying) {
+ this._open(null, callback);
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ },
+
+ /**
+ * Get or set options. For internal use and advanced users only.
+ *
+ * @param {string} o Option name
+ * @param {mixed} val optional A new value for the option
+ * @return {mixed|self} If val is omitted, the value of the option
+ * is returned, otherwise the instance itself is returned
+ * @public
+ */
+ option: function(o, val) {
+
+ // getter
+ if (val === undefined) {
+ return this.__options[o];
+ }
+ // setter
+ else {
+
+ if (!this.__destroyed) {
+
+ // change value
+ this.__options[o] = val;
+
+ // format
+ this.__optionsFormat();
+
+ // re-prepare the triggers if needed
+ if ($.inArray(o, ['trigger', 'triggerClose', 'triggerOpen']) >= 0) {
+ this.__prepareOrigin();
+ }
+
+ if (o === 'selfDestruction') {
+ this.__prepareGC();
+ }
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ }
+ },
+
+ /**
+ * This method is in charge of setting the position and size properties of the tooltip.
+ * All the hard work is delegated to the display plugin.
+ * Note: The tooltip may be detached from the DOM at the moment the method is called
+ * but must be attached by the end of the method call.
+ *
+ * @param {object} event For internal use only. Defined if an event such as
+ * window resizing triggered the repositioning
+ * @param {boolean} tooltipIsDetached For internal use only. Set this to true if you
+ * know that the tooltip not being in the DOM is not an issue (typically when the
+ * tooltip element has just been created but has not been added to the DOM yet).
+ * @returns {self}
+ * @public
+ */
+ reposition: function(event, tooltipIsDetached) {
+
+ var self = this;
+
+ if (!self.__destroyed) {
+
+ // if the tooltip has not been removed from DOM manually (or if it
+ // has been detached on purpose)
+ if (bodyContains(self._$tooltip) || tooltipIsDetached) {
+
+ if (!tooltipIsDetached) {
+ // detach in case the tooltip overflows the window and adds
+ // scrollbars to it, so __geometry can be accurate
+ self._$tooltip.detach();
+ }
+
+ // refresh the geometry object before passing it as a helper
+ self.__Geometry = self.__geometry();
+
+ // let a plugin fo the rest
+ self._trigger({
+ type: 'reposition',
+ event: event,
+ helper: {
+ geo: self.__Geometry
+ }
+ });
+ }
+ }
+ else {
+ self.__destroyError();
+ }
+
+ return self;
+ },
+
+ /**
+ * Alias, deprecated in 4.0.0
+ *
+ * @param callback
+ * @returns {self}
+ * @public
+ */
+ show: function(callback) {
+ return this.open(callback);
+ },
+
+ /**
+ * Returns some properties about the instance
+ *
+ * @returns {object}
+ * @public
+ */
+ status: function() {
+
+ return {
+ destroyed: this.__destroyed,
+ destroying: this.__destroying,
+ enabled: this.__enabled,
+ open: this.__state !== 'closed',
+ state: this.__state
+ };
+ },
+
+ /**
+ * For public use only, not to be used by plugins
+ *
+ * @returns {self}
+ * @public
+ */
+ triggerHandler: function() {
+
+ if (!this.__destroyed) {
+ this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic, Array.prototype.slice.apply(arguments));
+ }
+ else {
+ this.__destroyError();
+ }
+
+ return this;
+ }
+};
+
+$.fn.tooltipster = function() {
+
+ // for using in closures
+ var args = Array.prototype.slice.apply(arguments),
+ // common mistake: an HTML element can't be in several tooltips at the same time
+ contentCloningWarning = 'You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.';
+
+ // this happens with $(sel).tooltipster(...) when $(sel) does not match anything
+ if (this.length === 0) {
+
+ // still chainable
+ return this;
+ }
+ // this happens when calling $(sel).tooltipster('methodName or options')
+ // where $(sel) matches one or more elements
+ else {
+
+ // method calls
+ if (typeof args[0] === 'string') {
+
+ var v = '#*$~&';
+
+ this.each(function() {
+
+ // retrieve the namepaces of the tooltip(s) that exist on that element.
+ // We will interact with the first tooltip only.
+ var ns = $(this).data('tooltipster-ns'),
+ // self represents the instance of the first tooltipster plugin
+ // associated to the current HTML object of the loop
+ self = ns ? $(this).data(ns[0]) : null;
+
+ // if the current element holds a tooltipster instance
+ if (self) {
+
+ if (typeof self[args[0]] === 'function') {
+
+ if ( this.length > 1
+ && args[0] == 'content'
+ && ( args[1] instanceof $
+ || (typeof args[1] == 'object' && args[1] != null && args[1].tagName)
+ )
+ && !self.__options.contentCloning
+ && self.__options.debug
+ ) {
+ console.log(contentCloningWarning);
+ }
+
+ // note : args[1] and args[2] may not be defined
+ var resp = self[args[0]](args[1], args[2]);
+ }
+ else {
+ throw new Error('Unknown method "'+ args[0] +'"');
+ }
+
+ // if the function returned anything other than the instance
+ // itself (which implies chaining, except for the `instance` method)
+ if (resp !== self || args[0] === 'instance') {
+
+ v = resp;
+
+ // return false to stop .each iteration on the first element
+ // matched by the selector
+ return false;
+ }
+ }
+ else {
+ throw new Error('You called Tooltipster\'s "'+ args[0] +'" method on an uninitialized element');
+ }
+ });
+
+ return (v !== '#*$~&') ? v : this;
+ }
+ // first argument is undefined or an object: the tooltip is initializing
+ else {
+
+ // reset the array of last initialized objects
+ $.tooltipster.__instancesLatestArr = [];
+
+ // is there a defined value for the multiple option in the options object ?
+ var multipleIsSet = args[0] && args[0].multiple !== undefined,
+ // if the multiple option is set to true, or if it's not defined but
+ // set to true in the defaults
+ multiple = (multipleIsSet && args[0].multiple) || (!multipleIsSet && defaults.multiple),
+ // same for content
+ contentIsSet = args[0] && args[0].content !== undefined,
+ content = (contentIsSet && args[0].content) || (!contentIsSet && defaults.content),
+ // same for contentCloning
+ contentCloningIsSet = args[0] && args[0].contentCloning !== undefined,
+ contentCloning =
+ (contentCloningIsSet && args[0].contentCloning)
+ || (!contentCloningIsSet && defaults.contentCloning),
+ // same for debug
+ debugIsSet = args[0] && args[0].debug !== undefined,
+ debug = (debugIsSet && args[0].debug) || (!debugIsSet && defaults.debug);
+
+ if ( this.length > 1
+ && ( content instanceof $
+ || (typeof content == 'object' && content != null && content.tagName)
+ )
+ && !contentCloning
+ && debug
+ ) {
+ console.log(contentCloningWarning);
+ }
+
+ // create a tooltipster instance for each element if it doesn't
+ // already have one or if the multiple option is set, and attach the
+ // object to it
+ this.each(function() {
+
+ var go = false,
+ $this = $(this),
+ ns = $this.data('tooltipster-ns'),
+ obj = null;
+
+ if (!ns) {
+ go = true;
+ }
+ else if (multiple) {
+ go = true;
+ }
+ else if (debug) {
+ console.log('Tooltipster: one or more tooltips are already attached to the element below. Ignoring.');
+ console.log(this);
+ }
+
+ if (go) {
+ obj = new $.Tooltipster(this, args[0]);
+
+ // save the reference of the new instance
+ if (!ns) ns = [];
+ ns.push(obj.__namespace);
+ $this.data('tooltipster-ns', ns);
+
+ // save the instance itself
+ $this.data(obj.__namespace, obj);
+
+ // call our constructor custom function.
+ // we do this here and not in ::init() because we wanted
+ // the object to be saved in $this.data before triggering
+ // it
+ if (obj.__options.functionInit) {
+ obj.__options.functionInit.call(obj, obj, {
+ origin: this
+ });
+ }
+
+ // and now the event, for the plugins and core emitter
+ obj._trigger('init');
+ }
+
+ $.tooltipster.__instancesLatestArr.push(obj);
+ });
+
+ return this;
+ }
+ }
+};
+
+// Utilities
+
+/**
+ * A class to check if a tooltip can fit in given dimensions
+ *
+ * @param {object} $tooltip The jQuery wrapped tooltip element, or a clone of it
+ */
+function Ruler($tooltip) {
+
+ // list of instance variables
+
+ this.$container;
+ this.constraints = null;
+ this.__$tooltip;
+
+ this.__init($tooltip);
+}
+
+Ruler.prototype = {
+
+ /**
+ * Move the tooltip into an invisible div that does not allow overflow to make
+ * size tests. Note: the tooltip may or may not be attached to the DOM at the
+ * moment this method is called, it does not matter.
+ *
+ * @param {object} $tooltip The object to test. May be just a clone of the
+ * actual tooltip.
+ * @private
+ */
+ __init: function($tooltip) {
+
+ this.__$tooltip = $tooltip;
+
+ this.__$tooltip
+ .css({
+ // for some reason we have to specify top and left 0
+ left: 0,
+ // any overflow will be ignored while measuring
+ overflow: 'hidden',
+ // positions at (0,0) without the div using 100% of the available width
+ position: 'absolute',
+ top: 0
+ })
+ // overflow must be auto during the test. We re-set this in case
+ // it were modified by the user
+ .find('.tooltipster-content')
+ .css('overflow', 'auto');
+
+ this.$container = $('')
+ .append(this.__$tooltip)
+ .appendTo('body');
+ },
+
+ /**
+ * Force the browser to redraw (re-render) the tooltip immediately. This is required
+ * when you changed some CSS properties and need to make something with it
+ * immediately, without waiting for the browser to redraw at the end of instructions.
+ *
+ * @see http://stackoverflow.com/questions/3485365/how-can-i-force-webkit-to-redraw-repaint-to-propagate-style-changes
+ * @private
+ */
+ __forceRedraw: function() {
+
+ // note: this would work but for Webkit only
+ //this.__$tooltip.close();
+ //this.__$tooltip[0].offsetHeight;
+ //this.__$tooltip.open();
+
+ // works in FF too
+ var $p = this.__$tooltip.parent();
+ this.__$tooltip.detach();
+ this.__$tooltip.appendTo($p);
+ },
+
+ /**
+ * Set maximum dimensions for the tooltip. A call to ::measure afterwards
+ * will tell us if the content overflows or if it's ok
+ *
+ * @param {int} width
+ * @param {int} height
+ * @return {Ruler}
+ * @public
+ */
+ constrain: function(width, height) {
+
+ this.constraints = {
+ width: width,
+ height: height
+ };
+
+ this.__$tooltip.css({
+ // we disable display:flex, otherwise the content would overflow without
+ // creating horizontal scrolling (which we need to detect).
+ display: 'block',
+ // reset any previous height
+ height: '',
+ // we'll check if horizontal scrolling occurs
+ overflow: 'auto',
+ // we'll set the width and see what height is generated and if there
+ // is horizontal overflow
+ width: width
+ });
+
+ return this;
+ },
+
+ /**
+ * Reset the tooltip content overflow and remove the test container
+ *
+ * @returns {Ruler}
+ * @public
+ */
+ destroy: function() {
+
+ // in case the element was not a clone
+ this.__$tooltip
+ .detach()
+ .find('.tooltipster-content')
+ .css({
+ // reset to CSS value
+ display: '',
+ overflow: ''
+ });
+
+ this.$container.remove();
+ },
+
+ /**
+ * Removes any constraints
+ *
+ * @returns {Ruler}
+ * @public
+ */
+ free: function() {
+
+ this.constraints = null;
+
+ // reset to natural size
+ this.__$tooltip.css({
+ display: '',
+ height: '',
+ overflow: 'visible',
+ width: ''
+ });
+
+ return this;
+ },
+
+ /**
+ * Returns the size of the tooltip. When constraints are applied, also returns
+ * whether the tooltip fits in the provided dimensions.
+ * The idea is to see if the new height is small enough and if the content does
+ * not overflow horizontally.
+ *
+ * @param {int} width
+ * @param {int} height
+ * @returns {object} An object with a bool `fits` property and a `size` property
+ * @public
+ */
+ measure: function() {
+
+ this.__forceRedraw();
+
+ var tooltipBcr = this.__$tooltip[0].getBoundingClientRect(),
+ result = { size: {
+ // bcr.width/height are not defined in IE8- but in this
+ // case, bcr.right/bottom will have the same value
+ // except in iOS 8+ where tooltipBcr.bottom/right are wrong
+ // after scrolling for reasons yet to be determined
+ height: tooltipBcr.height || tooltipBcr.bottom,
+ width: tooltipBcr.width || tooltipBcr.right
+ }};
+
+ if (this.constraints) {
+
+ // note: we used to use offsetWidth instead of boundingRectClient but
+ // it returned rounded values, causing issues with sub-pixel layouts.
+
+ // note2: noticed that the bcrWidth of text content of a div was once
+ // greater than the bcrWidth of its container by 1px, causing the final
+ // tooltip box to be too small for its content. However, evaluating
+ // their widths one against the other (below) surprisingly returned
+ // equality. Happened only once in Chrome 48, was not able to reproduce
+ // => just having fun with float position values...
+
+ var $content = this.__$tooltip.find('.tooltipster-content'),
+ height = this.__$tooltip.outerHeight(),
+ contentBcr = $content[0].getBoundingClientRect(),
+ fits = {
+ height: height <= this.constraints.height,
+ width: (
+ // this condition accounts for min-width property that
+ // may apply
+ tooltipBcr.width <= this.constraints.width
+ // the -1 is here because scrollWidth actually returns
+ // a rounded value, and may be greater than bcr.width if
+ // it was rounded up. This may cause an issue for contents
+ // which actually really overflow by 1px or so, but that
+ // should be rare. Not sure how to solve this efficiently.
+ // See http://blogs.msdn.com/b/ie/archive/2012/02/17/sub-pixel-rendering-and-the-css-object-model.aspx
+ && contentBcr.width >= $content[0].scrollWidth - 1
+ )
+ };
+
+ result.fits = fits.height && fits.width;
+ }
+
+ // old versions of IE get the width wrong for some reason
+ if (env.IE && env.IE <= 11) {
+ result.size.width = Math.ceil(result.size.width) + 1;
+ }
+
+ return result;
+ }
+};
+
+// quick & dirty compare function, not bijective nor multidimensional
+function areEqual(a,b) {
+ var same = true;
+ $.each(a, function(i, _) {
+ if (b[i] === undefined || a[i] !== b[i]) {
+ same = false;
+ return false;
+ }
+ });
+ return same;
+}
+
+/**
+ * A fast function to check if an element is still in the DOM. It
+ * tries to use an id as ids are indexed by the browser, or falls
+ * back to jQuery's `contains` method. May fail if two elements
+ * have the same id, but so be it
+ *
+ * @param {object} $obj A jQuery-wrapped HTML element
+ * @return {boolean}
+ */
+function bodyContains($obj) {
+ var id = $obj.attr('id'),
+ el = id ? env.window.document.getElementById(id) : null;
+ // must also check that the element with the id is the one we want
+ return el ? el === $obj[0] : $.contains(env.window.document.body, $obj[0]);
+}
+
+// detect IE versions for dirty fixes
+var uA = navigator.userAgent.toLowerCase();
+if (uA.indexOf('msie') != -1) env.IE = parseInt(uA.split('msie')[1]);
+else if (uA.toLowerCase().indexOf('trident') !== -1 && uA.indexOf(' rv:11') !== -1) env.IE = 11;
+else if (uA.toLowerCase().indexOf('edge/') != -1) env.IE = parseInt(uA.toLowerCase().split('edge/')[1]);
+
+// detecting support for CSS transitions
+function transitionSupport() {
+
+ // env.window is not defined yet when this is called
+ if (!win) return false;
+
+ var b = win.document.body || win.document.documentElement,
+ s = b.style,
+ p = 'transition',
+ v = ['Moz', 'Webkit', 'Khtml', 'O', 'ms'];
+
+ if (typeof s[p] == 'string') { return true; }
+
+ p = p.charAt(0).toUpperCase() + p.substr(1);
+ for (var i=0; i0?e=c.__plugins[d]:a.each(c.__plugins,function(a,b){return b.name.substring(b.name.length-d.length-1)=="."+d?(e=b,!1):void 0}),e}if(b.name.indexOf(".")<0)throw new Error("Plugins must be namespaced");return c.__plugins[b.name]=b,b.core&&c.__bridge(b.core,c,b.name),this},_trigger:function(){var a=Array.prototype.slice.apply(arguments);return"string"==typeof a[0]&&(a[0]={type:a[0]}),this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,a),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,a),this},instances:function(b){var c=[],d=b||".tooltipstered";return a(d).each(function(){var b=a(this),d=b.data("tooltipster-ns");d&&a.each(d,function(a,d){c.push(b.data(d))})}),c},instancesLatest:function(){return this.__instancesLatestArr},off:function(){return this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},origins:function(b){var c=b?b+" ":"";return a(c+".tooltipstered").toArray()},setDefaults:function(b){return a.extend(f,b),this},triggerHandler:function(){return this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.tooltipster=new i,a.Tooltipster=function(b,c){this.__callbacks={close:[],open:[]},this.__closingTime,this.__Content,this.__contentBcr,this.__destroyed=!1,this.__destroying=!1,this.__$emitterPrivate=a({}),this.__$emitterPublic=a({}),this.__enabled=!0,this.__garbageCollector,this.__Geometry,this.__lastPosition,this.__namespace="tooltipster-"+Math.round(1e6*Math.random()),this.__options,this.__$originParents,this.__pointerIsOverOrigin=!1,this.__previousThemes=[],this.__state="closed",this.__timeouts={close:[],open:null},this.__touchEvents=[],this.__tracker=null,this._$origin,this._$tooltip,this.__init(b,c)},a.Tooltipster.prototype={__init:function(b,c){var d=this;if(d._$origin=a(b),d.__options=a.extend(!0,{},f,c),d.__optionsFormat(),!h.IE||h.IE>=d.__options.IEmin){var e=null;if(void 0===d._$origin.data("tooltipster-initialTitle")&&(e=d._$origin.attr("title"),void 0===e&&(e=null),d._$origin.data("tooltipster-initialTitle",e)),null!==d.__options.content)d.__contentSet(d.__options.content);else{var g,i=d._$origin.attr("data-tooltip-content");i&&(g=a(i)),g&&g[0]?d.__contentSet(g.first()):d.__contentSet(e)}d._$origin.removeAttr("title").addClass("tooltipstered"),d.__prepareOrigin(),d.__prepareGC(),a.each(d.__options.plugins,function(a,b){d._plug(b)}),h.hasTouchCapability&&a("body").on("touchmove."+d.__namespace+"-triggerOpen",function(a){d._touchRecordEvent(a)}),d._on("created",function(){d.__prepareTooltip()})._on("repositioned",function(a){d.__lastPosition=a.position})}else d.__options.disabled=!0},__contentInsert:function(){var a=this,b=a._$tooltip.find(".tooltipster-content"),c=a.__Content,d=function(a){c=a};return a._trigger({type:"format",content:a.__Content,format:d}),a.__options.functionFormat&&(c=a.__options.functionFormat.call(a,a,{origin:a._$origin[0]},a.__Content)),"string"!=typeof c||a.__options.contentAsHTML?b.empty().append(c):b.text(c),a},__contentSet:function(b){return b instanceof a&&this.__options.contentCloning&&(b=b.clone(!0)),this.__Content=b,this._trigger({type:"updated",content:b}),this},__destroyError:function(){throw new Error("This tooltip has been destroyed and cannot execute your method call.")},__geometry:function(){var b=this,c=b._$origin,d=b._$origin.is("area");if(d){var e=b._$origin.parent().attr("name");c=a('img[usemap="#'+e+'"]')}var f=c[0].getBoundingClientRect(),g=a(h.window.document),i=a(h.window),j=c,k={available:{document:null,window:null},document:{size:{height:g.height(),width:g.width()}},window:{scroll:{left:h.window.scrollX||h.window.document.documentElement.scrollLeft,top:h.window.scrollY||h.window.document.documentElement.scrollTop},size:{height:i.height(),width:i.width()}},origin:{fixedLineage:!1,offset:{},size:{height:f.bottom-f.top,width:f.right-f.left},usemapImage:d?c[0]:null,windowOffset:{bottom:f.bottom,left:f.left,right:f.right,top:f.top}}};if(d){var l=b._$origin.attr("shape"),m=b._$origin.attr("coords");if(m&&(m=m.split(","),a.map(m,function(a,b){m[b]=parseInt(a)})),"default"!=l)switch(l){case"circle":var n=m[0],o=m[1],p=m[2],q=o-p,r=n-p;k.origin.size.height=2*p,k.origin.size.width=k.origin.size.height,k.origin.windowOffset.left+=r,k.origin.windowOffset.top+=q;break;case"rect":var s=m[0],t=m[1],u=m[2],v=m[3];k.origin.size.height=v-t,k.origin.size.width=u-s,k.origin.windowOffset.left+=s,k.origin.windowOffset.top+=t;break;case"poly":for(var w=0,x=0,y=0,z=0,A="even",B=0;By&&(y=C,0===B&&(w=y)),w>C&&(w=C),A="odd"):(C>z&&(z=C,1==B&&(x=z)),x>C&&(x=C),A="even")}k.origin.size.height=z-x,k.origin.size.width=y-w,k.origin.windowOffset.left+=w,k.origin.windowOffset.top+=x}}var D=function(a){k.origin.size.height=a.height,k.origin.windowOffset.left=a.left,k.origin.windowOffset.top=a.top,k.origin.size.width=a.width};for(b._trigger({type:"geometry",edit:D,geometry:{height:k.origin.size.height,left:k.origin.windowOffset.left,top:k.origin.windowOffset.top,width:k.origin.size.width}}),k.origin.windowOffset.right=k.origin.windowOffset.left+k.origin.size.width,k.origin.windowOffset.bottom=k.origin.windowOffset.top+k.origin.size.height,k.origin.offset.left=k.origin.windowOffset.left+h.window.scrollX,k.origin.offset.top=k.origin.windowOffset.top+h.window.scrollY,k.origin.offset.bottom=k.origin.offset.top+k.origin.size.height,k.origin.offset.right=k.origin.offset.left+k.origin.size.width,k.available.document={bottom:{height:k.document.size.height-k.origin.offset.bottom,width:k.document.size.width},left:{height:k.document.size.height,width:k.origin.offset.left},right:{height:k.document.size.height,width:k.document.size.width-k.origin.offset.right},top:{height:k.origin.offset.top,width:k.document.size.width}},k.available.window={bottom:{height:Math.max(k.window.size.height-Math.max(k.origin.windowOffset.bottom,0),0),width:k.window.size.width},left:{height:k.window.size.height,width:Math.max(k.origin.windowOffset.left,0)},right:{height:k.window.size.height,width:Math.max(k.window.size.width-Math.max(k.origin.windowOffset.right,0),0)},top:{height:Math.max(k.origin.windowOffset.top,0),width:k.window.size.width}};"html"!=j[0].tagName.toLowerCase();){if("fixed"==j.css("position")){k.origin.fixedLineage=!0;break}j=j.parent()}return k},__optionsFormat:function(){return"number"==typeof this.__options.animationDuration&&(this.__options.animationDuration=[this.__options.animationDuration,this.__options.animationDuration]),"number"==typeof this.__options.delay&&(this.__options.delay=[this.__options.delay,this.__options.delay]),"number"==typeof this.__options.delayTouch&&(this.__options.delayTouch=[this.__options.delayTouch,this.__options.delayTouch]),"string"==typeof this.__options.theme&&(this.__options.theme=[this.__options.theme]),"string"==typeof this.__options.parent&&(this.__options.parent=a(this.__options.parent)),"hover"==this.__options.trigger?(this.__options.triggerOpen={mouseenter:!0,touchstart:!0},this.__options.triggerClose={mouseleave:!0,originClick:!0,touchleave:!0}):"click"==this.__options.trigger&&(this.__options.triggerOpen={click:!0,tap:!0},this.__options.triggerClose={click:!0,tap:!0}),this._trigger("options"),this},__prepareGC:function(){var b=this;return b.__options.selfDestruction?b.__garbageCollector=setInterval(function(){var c=(new Date).getTime();b.__touchEvents=a.grep(b.__touchEvents,function(a,b){return c-a.time>6e4}),d(b._$origin)||b.destroy()},2e4):clearInterval(b.__garbageCollector),b},__prepareOrigin:function(){var a=this;if(a._$origin.off("."+a.__namespace+"-triggerOpen"),h.hasTouchCapability&&a._$origin.on("touchstart."+a.__namespace+"-triggerOpen touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen",function(b){a._touchRecordEvent(b)}),a.__options.triggerOpen.click||a.__options.triggerOpen.tap&&h.hasTouchCapability){var b="";a.__options.triggerOpen.click&&(b+="click."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.tap&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&a._open(b)})}if(a.__options.triggerOpen.mouseenter||a.__options.triggerOpen.touchstart&&h.hasTouchCapability){var b="";a.__options.triggerOpen.mouseenter&&(b+="mouseenter."+a.__namespace+"-triggerOpen "),a.__options.triggerOpen.touchstart&&h.hasTouchCapability&&(b+="touchstart."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){!a._touchIsTouchEvent(b)&&a._touchIsEmulatedEvent(b)||(a.__pointerIsOverOrigin=!0,a._openShortly(b))})}if(a.__options.triggerClose.mouseleave||a.__options.triggerClose.touchleave&&h.hasTouchCapability){var b="";a.__options.triggerClose.mouseleave&&(b+="mouseleave."+a.__namespace+"-triggerOpen "),a.__options.triggerClose.touchleave&&h.hasTouchCapability&&(b+="touchend."+a.__namespace+"-triggerOpen touchcancel."+a.__namespace+"-triggerOpen"),a._$origin.on(b,function(b){a._touchIsMeaningfulEvent(b)&&(a.__pointerIsOverOrigin=!1)})}return a},__prepareTooltip:function(){var b=this,c=b.__options.interactive?"auto":"";return b._$tooltip.attr("id",b.__namespace).css({"pointer-events":c,zIndex:b.__options.zIndex}),a.each(b.__previousThemes,function(a,c){b._$tooltip.removeClass(c)}),a.each(b.__options.theme,function(a,c){b._$tooltip.addClass(c)}),b.__previousThemes=a.merge([],b.__options.theme),b},__scrollHandler:function(b){var c=this;if(c.__options.triggerClose.scroll)c._close(b);else{if(b.target===h.window.document)c.__Geometry.origin.fixedLineage||c.__options.repositionOnScroll&&c.reposition(b);else{var d=c.__geometry(),e=!1;if("fixed"!=c._$origin.css("position")&&c.__$originParents.each(function(b,c){var f=a(c),g=f.css("overflow-x"),h=f.css("overflow-y");if("visible"!=g||"visible"!=h){var i=c.getBoundingClientRect();if("visible"!=g&&(d.origin.windowOffset.lefti.right))return e=!0,!1;if("visible"!=h&&(d.origin.windowOffset.topi.bottom))return e=!0,!1}return"fixed"==f.css("position")?!1:void 0}),e)c._$tooltip.css("visibility","hidden");else if(c._$tooltip.css("visibility","visible"),c.__options.repositionOnScroll)c.reposition(b);else{var f=d.origin.offset.left-c.__Geometry.origin.offset.left,g=d.origin.offset.top-c.__Geometry.origin.offset.top;c._$tooltip.css({left:c.__lastPosition.coord.left+f,top:c.__lastPosition.coord.top+g})}}c._trigger({type:"scroll",event:b})}return c},__stateSet:function(a){return this.__state=a,this._trigger({type:"state",state:a}),this},__timeoutsClear:function(){return clearTimeout(this.__timeouts.open),this.__timeouts.open=null,a.each(this.__timeouts.close,function(a,b){clearTimeout(b)}),this.__timeouts.close=[],this},__trackerStart:function(){var a=this,b=a._$tooltip.find(".tooltipster-content");return a.__options.trackTooltip&&(a.__contentBcr=b[0].getBoundingClientRect()),a.__tracker=setInterval(function(){if(d(a._$origin)&&d(a._$tooltip)){if(a.__options.trackOrigin){var e=a.__geometry(),f=!1;c(e.origin.size,a.__Geometry.origin.size)&&(a.__Geometry.origin.fixedLineage?c(e.origin.windowOffset,a.__Geometry.origin.windowOffset)&&(f=!0):c(e.origin.offset,a.__Geometry.origin.offset)&&(f=!0)),f||(a.__options.triggerClose.mouseleave?a._close():a.reposition())}if(a.__options.trackTooltip){var g=b[0].getBoundingClientRect();g.height===a.__contentBcr.height&&g.width===a.__contentBcr.width||(a.reposition(),a.__contentBcr=g)}}else a._close()},a.__options.trackerInterval),a},_close:function(b,c){var d=this,e=!0;if(d._trigger({type:"close",event:b,stop:function(){e=!1}}),e||d.__destroying){c&&d.__callbacks.close.push(c),d.__callbacks.open=[],d.__timeoutsClear();var f=function(){a.each(d.__callbacks.close,function(a,c){c.call(d,d,{event:b,origin:d._$origin[0]})}),d.__callbacks.close=[]};if("closed"!=d.__state){var g=!0,i=new Date,j=i.getTime(),k=j+d.__options.animationDuration[1];if("disappearing"==d.__state&&k>d.__closingTime&&(g=!1),g){d.__closingTime=k,"disappearing"!=d.__state&&d.__stateSet("disappearing");var l=function(){clearInterval(d.__tracker),d._trigger({type:"closing",event:b}),d._$tooltip.off("."+d.__namespace+"-triggerClose").removeClass("tooltipster-dying"),a(h.window).off("."+d.__namespace+"-triggerClose"),d.__$originParents.each(function(b,c){a(c).off("scroll."+d.__namespace+"-triggerClose")}),d.__$originParents=null,a("body").off("."+d.__namespace+"-triggerClose"),d._$origin.off("."+d.__namespace+"-triggerClose"),d._off("dismissable"),d.__stateSet("closed"),d._trigger({type:"after",event:b}),d.__options.functionAfter&&d.__options.functionAfter.call(d,d,{event:b}),f()};h.hasTransitions?(d._$tooltip.css({"-moz-animation-duration":d.__options.animationDuration[1]+"ms","-ms-animation-duration":d.__options.animationDuration[1]+"ms","-o-animation-duration":d.__options.animationDuration[1]+"ms","-webkit-animation-duration":d.__options.animationDuration[1]+"ms","animation-duration":d.__options.animationDuration[1]+"ms","transition-duration":d.__options.animationDuration[1]+"ms"}),d._$tooltip.clearQueue().removeClass("tooltipster-show").addClass("tooltipster-dying"),d.__options.animationDuration[1]>0&&d._$tooltip.delay(d.__options.animationDuration[1]),d._$tooltip.queue(l)):d._$tooltip.stop().fadeOut(d.__options.animationDuration[1],l)}}else f()}return d},_off:function(){return this.__$emitterPrivate.off.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_on:function(){return this.__$emitterPrivate.on.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_one:function(){return this.__$emitterPrivate.one.apply(this.__$emitterPrivate,Array.prototype.slice.apply(arguments)),this},_open:function(b,c){var e=this;if(!e.__destroying&&d(e._$origin)&&e.__enabled){var f=!0;if("closed"==e.__state&&(e._trigger({type:"before",event:b,stop:function(){f=!1}}),f&&e.__options.functionBefore&&(f=e.__options.functionBefore.call(e,e,{event:b,origin:e._$origin[0]}))),f!==!1&&null!==e.__Content){c&&e.__callbacks.open.push(c),e.__callbacks.close=[],e.__timeoutsClear();var g,i=function(){"stable"!=e.__state&&e.__stateSet("stable"),a.each(e.__callbacks.open,function(a,b){b.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}),e.__callbacks.open=[]};if("closed"!==e.__state)g=0,"disappearing"===e.__state?(e.__stateSet("appearing"),h.hasTransitions?(e._$tooltip.clearQueue().removeClass("tooltipster-dying").addClass("tooltipster-show"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i)):e._$tooltip.stop().fadeIn(i)):"stable"==e.__state&&i();else{if(e.__stateSet("appearing"),g=e.__options.animationDuration[0],e.__contentInsert(),e.reposition(b,!0),h.hasTransitions?(e._$tooltip.addClass("tooltipster-"+e.__options.animation).addClass("tooltipster-initial").css({"-moz-animation-duration":e.__options.animationDuration[0]+"ms","-ms-animation-duration":e.__options.animationDuration[0]+"ms","-o-animation-duration":e.__options.animationDuration[0]+"ms","-webkit-animation-duration":e.__options.animationDuration[0]+"ms","animation-duration":e.__options.animationDuration[0]+"ms","transition-duration":e.__options.animationDuration[0]+"ms"}),setTimeout(function(){"closed"!=e.__state&&(e._$tooltip.addClass("tooltipster-show").removeClass("tooltipster-initial"),e.__options.animationDuration[0]>0&&e._$tooltip.delay(e.__options.animationDuration[0]),e._$tooltip.queue(i))},0)):e._$tooltip.css("display","none").fadeIn(e.__options.animationDuration[0],i),e.__trackerStart(),a(h.window).on("resize."+e.__namespace+"-triggerClose",function(a){e.reposition(a)}).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)}),e.__$originParents=e._$origin.parents(),e.__$originParents.each(function(b,c){a(c).on("scroll."+e.__namespace+"-triggerClose",function(a){e.__scrollHandler(a)})}),e.__options.triggerClose.mouseleave||e.__options.triggerClose.touchleave&&h.hasTouchCapability){e._on("dismissable",function(a){a.dismissable?a.delay?(m=setTimeout(function(){e._close(a.event)},a.delay),e.__timeouts.close.push(m)):e._close(a):clearTimeout(m)});var j=e._$origin,k="",l="",m=null;e.__options.interactive&&(j=j.add(e._$tooltip)),e.__options.triggerClose.mouseleave&&(k+="mouseenter."+e.__namespace+"-triggerClose ",l+="mouseleave."+e.__namespace+"-triggerClose "),e.__options.triggerClose.touchleave&&h.hasTouchCapability&&(k+="touchstart."+e.__namespace+"-triggerClose",l+="touchend."+e.__namespace+"-triggerClose touchcancel."+e.__namespace+"-triggerClose"),j.on(l,function(a){if(e._touchIsTouchEvent(a)||!e._touchIsEmulatedEvent(a)){var b="mouseleave"==a.type?e.__options.delay:e.__options.delayTouch;e._trigger({delay:b[1],dismissable:!0,event:a,type:"dismissable"})}}).on(k,function(a){!e._touchIsTouchEvent(a)&&e._touchIsEmulatedEvent(a)||e._trigger({dismissable:!1,event:a,type:"dismissable"})})}e.__options.triggerClose.originClick&&e._$origin.on("click."+e.__namespace+"-triggerClose",function(a){e._touchIsTouchEvent(a)||e._touchIsEmulatedEvent(a)||e._close(a)}),(e.__options.triggerClose.click||e.__options.triggerClose.tap&&h.hasTouchCapability)&&setTimeout(function(){if("closed"!=e.__state){var b="";e.__options.triggerClose.click&&(b+="click."+e.__namespace+"-triggerClose "),e.__options.triggerClose.tap&&h.hasTouchCapability&&(b+="touchend."+e.__namespace+"-triggerClose"),a("body").on(b,function(b){e._touchIsMeaningfulEvent(b)&&(e._touchRecordEvent(b),e.__options.interactive&&a.contains(e._$tooltip[0],b.target)||e._close(b))}),e.__options.triggerClose.tap&&h.hasTouchCapability&&a("body").on("touchstart."+e.__namespace+"-triggerClose",function(a){e._touchRecordEvent(a)})}},0),e._trigger("ready"),e.__options.functionReady&&e.__options.functionReady.call(e,e,{origin:e._$origin[0],tooltip:e._$tooltip[0]})}if(e.__options.timer>0){var m=setTimeout(function(){e._close()},e.__options.timer+g);e.__timeouts.close.push(m)}}}return e},_openShortly:function(a){var b=this,c=!0;if("stable"!=b.__state&&"appearing"!=b.__state&&!b.__timeouts.open&&(b._trigger({type:"start",event:a,stop:function(){c=!1}}),c)){var d=0==a.type.indexOf("touch")?b.__options.delayTouch:b.__options.delay;d[0]?b.__timeouts.open=setTimeout(function(){b.__timeouts.open=null,b.__pointerIsOverOrigin&&b._touchIsMeaningfulEvent(a)?(b._trigger("startend"),b._open(a)):b._trigger("startcancel")},d[0]):(b._trigger("startend"),b._open(a))}return b},_optionsExtract:function(b,c){var d=this,e=a.extend(!0,{},c),f=d.__options[b];return f||(f={},a.each(c,function(a,b){var c=d.__options[a];void 0!==c&&(f[a]=c)})),a.each(e,function(b,c){void 0!==f[b]&&("object"!=typeof c||c instanceof Array||null==c||"object"!=typeof f[b]||f[b]instanceof Array||null==f[b]?e[b]=f[b]:a.extend(e[b],f[b]))}),e},_plug:function(b){var c=a.tooltipster._plugin(b);if(!c)throw new Error('The "'+b+'" plugin is not defined');return c.instance&&a.tooltipster.__bridge(c.instance,this,c.name),this},_touchIsEmulatedEvent:function(a){for(var b=!1,c=(new Date).getTime(),d=this.__touchEvents.length-1;d>=0;d--){var e=this.__touchEvents[d];if(!(c-e.time<500))break;e.target===a.target&&(b=!0)}return b},_touchIsMeaningfulEvent:function(a){return this._touchIsTouchEvent(a)&&!this._touchSwiped(a.target)||!this._touchIsTouchEvent(a)&&!this._touchIsEmulatedEvent(a)},_touchIsTouchEvent:function(a){return 0==a.type.indexOf("touch")},_touchRecordEvent:function(a){return this._touchIsTouchEvent(a)&&(a.time=(new Date).getTime(),this.__touchEvents.push(a)),this},_touchSwiped:function(a){for(var b=!1,c=this.__touchEvents.length-1;c>=0;c--){var d=this.__touchEvents[c];if("touchmove"==d.type){b=!0;break}if("touchstart"==d.type&&a===d.target)break}return b},_trigger:function(){var b=Array.prototype.slice.apply(arguments);return"string"==typeof b[0]&&(b[0]={type:b[0]}),b[0].instance=this,b[0].origin=this._$origin?this._$origin[0]:null,b[0].tooltip=this._$tooltip?this._$tooltip[0]:null,this.__$emitterPrivate.trigger.apply(this.__$emitterPrivate,b),a.tooltipster._trigger.apply(a.tooltipster,b),this.__$emitterPublic.trigger.apply(this.__$emitterPublic,b),this},_unplug:function(b){var c=this;if(c[b]){var d=a.tooltipster._plugin(b);d.instance&&a.each(d.instance,function(a,d){c[a]&&c[a].bridged===c[b]&&delete c[a]}),c[b].__destroy&&c[b].__destroy(),delete c[b]}return c},close:function(a){return this.__destroyed?this.__destroyError():this._close(null,a),this},content:function(a){var b=this;if(void 0===a)return b.__Content;if(b.__destroyed)b.__destroyError();else if(b.__contentSet(a),null!==b.__Content){if("closed"!==b.__state&&(b.__contentInsert(),b.reposition(),b.__options.updateAnimation))if(h.hasTransitions){var c=b.__options.updateAnimation;b._$tooltip.addClass("tooltipster-update-"+c),setTimeout(function(){"closed"!=b.__state&&b._$tooltip.removeClass("tooltipster-update-"+c)},1e3)}else b._$tooltip.fadeTo(200,.5,function(){"closed"!=b.__state&&b._$tooltip.fadeTo(200,1)})}else b._close();return b},destroy:function(){var b=this;return b.__destroyed?b.__destroyError():b.__destroying||(b.__destroying=!0,b._close(null,function(){b._trigger("destroy"),b.__destroying=!1,b.__destroyed=!0,b._$origin.removeData(b.__namespace).off("."+b.__namespace+"-triggerOpen"),a("body").off("."+b.__namespace+"-triggerOpen");var c=b._$origin.data("tooltipster-ns");if(c)if(1===c.length){var d=null;"previous"==b.__options.restoration?d=b._$origin.data("tooltipster-initialTitle"):"current"==b.__options.restoration&&(d="string"==typeof b.__Content?b.__Content:a("").append(b.__Content).html()),d&&b._$origin.attr("title",d),b._$origin.removeClass("tooltipstered"),b._$origin.removeData("tooltipster-ns").removeData("tooltipster-initialTitle")}else c=a.grep(c,function(a,c){return a!==b.__namespace}),b._$origin.data("tooltipster-ns",c);b._trigger("destroyed"),b._off(),b.off(),b.__Content=null,b.__$emitterPrivate=null,b.__$emitterPublic=null,b.__options.parent=null,b._$origin=null,b._$tooltip=null,a.tooltipster.__instancesLatestArr=a.grep(a.tooltipster.__instancesLatestArr,function(a,c){return b!==a}),clearInterval(b.__garbageCollector)})),b},disable:function(){return this.__destroyed?(this.__destroyError(),this):(this._close(),this.__enabled=!1,this)},elementOrigin:function(){return this.__destroyed?void this.__destroyError():this._$origin[0]},elementTooltip:function(){return this._$tooltip?this._$tooltip[0]:null},enable:function(){return this.__enabled=!0,this},hide:function(a){return this.close(a)},instance:function(){return this},off:function(){return this.__destroyed||this.__$emitterPublic.off.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},on:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.on.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},one:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.one.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this},open:function(a){return this.__destroyed||this.__destroying?this.__destroyError():this._open(null,a),this},option:function(b,c){return void 0===c?this.__options[b]:(this.__destroyed?this.__destroyError():(this.__options[b]=c,this.__optionsFormat(),a.inArray(b,["trigger","triggerClose","triggerOpen"])>=0&&this.__prepareOrigin(),"selfDestruction"===b&&this.__prepareGC()),this)},reposition:function(a,b){var c=this;return c.__destroyed?c.__destroyError():(d(c._$tooltip)||b)&&(b||c._$tooltip.detach(),c.__Geometry=c.__geometry(),c._trigger({type:"reposition",event:a,helper:{geo:c.__Geometry}})),c},show:function(a){return this.open(a)},status:function(){return{destroyed:this.__destroyed,destroying:this.__destroying,enabled:this.__enabled,open:"closed"!==this.__state,state:this.__state}},triggerHandler:function(){return this.__destroyed?this.__destroyError():this.__$emitterPublic.triggerHandler.apply(this.__$emitterPublic,Array.prototype.slice.apply(arguments)),this}},a.fn.tooltipster=function(){var b=Array.prototype.slice.apply(arguments),c="You are using a single HTML element as content for several tooltips. You probably want to set the contentCloning option to TRUE.";if(0===this.length)return this;if("string"==typeof b[0]){var d="#*$~&";return this.each(function(){var e=a(this).data("tooltipster-ns"),f=e?a(this).data(e[0]):null;if(!f)throw new Error("You called Tooltipster's \""+b[0]+'" method on an uninitialized element');if("function"!=typeof f[b[0]])throw new Error('Unknown method "'+b[0]+'"');this.length>1&&"content"==b[0]&&(b[1]instanceof a||"object"==typeof b[1]&&null!=b[1]&&b[1].tagName)&&!f.__options.contentCloning&&f.__options.debug&&console.log(c);var g=f[b[0]](b[1],b[2]);return g!==f||"instance"===b[0]?(d=g,!1):void 0}),"#*$~&"!==d?d:this}a.tooltipster.__instancesLatestArr=[];var e=b[0]&&void 0!==b[0].multiple,g=e&&b[0].multiple||!e&&f.multiple,h=b[0]&&void 0!==b[0].content,i=h&&b[0].content||!h&&f.content,j=b[0]&&void 0!==b[0].contentCloning,k=j&&b[0].contentCloning||!j&&f.contentCloning,l=b[0]&&void 0!==b[0].debug,m=l&&b[0].debug||!l&&f.debug;return this.length>1&&(i instanceof a||"object"==typeof i&&null!=i&&i.tagName)&&!k&&m&&console.log(c),this.each(function(){var c=!1,d=a(this),e=d.data("tooltipster-ns"),f=null;e?g?c=!0:m&&(console.log("Tooltipster: one or more tooltips are already attached to the element below. Ignoring."),console.log(this)):c=!0,c&&(f=new a.Tooltipster(this,b[0]),e||(e=[]),e.push(f.__namespace),d.data("tooltipster-ns",e),d.data(f.__namespace,f),f.__options.functionInit&&f.__options.functionInit.call(f,f,{origin:this}),f._trigger("init")),a.tooltipster.__instancesLatestArr.push(f)}),this},b.prototype={__init:function(b){this.__$tooltip=b,this.__$tooltip.css({left:0,overflow:"hidden",position:"absolute",top:0}).find(".tooltipster-content").css("overflow","auto"),this.$container=a('').append(this.__$tooltip).appendTo("body")},__forceRedraw:function(){var a=this.__$tooltip.parent();this.__$tooltip.detach(),this.__$tooltip.appendTo(a)},constrain:function(a,b){return this.constraints={width:a,height:b},this.__$tooltip.css({display:"block",height:"",overflow:"auto",width:a}),this},destroy:function(){this.__$tooltip.detach().find(".tooltipster-content").css({display:"",overflow:""}),this.$container.remove()},free:function(){return this.constraints=null,this.__$tooltip.css({display:"",height:"",overflow:"visible",width:""}),this},measure:function(){this.__forceRedraw();var a=this.__$tooltip[0].getBoundingClientRect(),b={size:{height:a.height||a.bottom,width:a.width||a.right}};if(this.constraints){var c=this.__$tooltip.find(".tooltipster-content"),d=this.__$tooltip.outerHeight(),e=c[0].getBoundingClientRect(),f={height:d<=this.constraints.height,width:a.width<=this.constraints.width&&e.width>=c[0].scrollWidth-1};b.fits=f.height&&f.width}return h.IE&&h.IE<=11&&(b.size.width=Math.ceil(b.size.width)+1),b}};var j=navigator.userAgent.toLowerCase();-1!=j.indexOf("msie")?h.IE=parseInt(j.split("msie")[1]):-1!==j.toLowerCase().indexOf("trident")&&-1!==j.indexOf(" rv:11")?h.IE=11:-1!=j.toLowerCase().indexOf("edge/")&&(h.IE=parseInt(j.toLowerCase().split("edge/")[1]))});
\ No newline at end of file
diff --git a/PlexRequests.UI/Helpers/BaseUrlHelper.cs b/PlexRequests.UI/Helpers/BaseUrlHelper.cs
index f2d42a432..199ba20b9 100644
--- a/PlexRequests.UI/Helpers/BaseUrlHelper.cs
+++ b/PlexRequests.UI/Helpers/BaseUrlHelper.cs
@@ -83,7 +83,8 @@ namespace PlexRequests.UI.Helpers
$"",
$"",
$"",
- $""
+ $"",
+ $"",
};
@@ -98,7 +99,8 @@ namespace PlexRequests.UI.Helpers
$"",
$"",
$"",
- $""
+ $"",
+ $""
};
diff --git a/PlexRequests.UI/Modules/SearchModule.cs b/PlexRequests.UI/Modules/SearchModule.cs
index 4597360b6..090fef703 100644
--- a/PlexRequests.UI/Modules/SearchModule.cs
+++ b/PlexRequests.UI/Modules/SearchModule.cs
@@ -336,7 +336,7 @@ namespace PlexRequests.UI.Modules
SeriesName = t.show.name,
Status = t.show.status
};
-
+
if (plexSettings.AdvancedSearch)
{
@@ -664,9 +664,10 @@ namespace PlexRequests.UI.Modules
break;
case "episode":
model.Episodes = new List();
- for (var i = 0; i < episodeModel.Episodes.Length; i++)
+
+ foreach (var ep in episodeModel?.Episodes)
{
- model.Episodes[i] = new EpisodesModel { EpisodeNumber = episodeModel.Episodes[i].EpisodeNumber, SeasonNumber = episodeModel.Episodes[i].SeasonNumber };
+ model.Episodes.Add(new EpisodesModel { EpisodeNumber = ep.EpisodeNumber, SeasonNumber = ep.SeasonNumber });
}
break;
default:
diff --git a/PlexRequests.UI/PlexRequests.UI.csproj b/PlexRequests.UI/PlexRequests.UI.csproj
index a69475ce9..7c7a42bb1 100644
--- a/PlexRequests.UI/PlexRequests.UI.csproj
+++ b/PlexRequests.UI/PlexRequests.UI.csproj
@@ -384,6 +384,54 @@
PreserveNewest
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
+
+ Always
+
diff --git a/PlexRequests.UI/Views/Requests/Index.cshtml b/PlexRequests.UI/Views/Requests/Index.cshtml
index 898e58781..9c6b7f7fe 100644
--- a/PlexRequests.UI/Views/Requests/Index.cshtml
+++ b/PlexRequests.UI/Views/Requests/Index.cshtml
@@ -22,7 +22,7 @@
}
@if (Model.SearchForTvShows)
{
-