Skip to content

Instantly share code, notes, and snippets.

@BrokenEagle
Last active October 8, 2019 01:40
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save BrokenEagle/dd9445d7e83d136716bb742228e50259 to your computer and use it in GitHub Desktop.
Save BrokenEagle/dd9445d7e83d136716bb742228e50259 to your computer and use it in GitHub Desktop.
(Unsupported, see comments): Twitter Image Searches and Stuff. Beta test before moving to the main GitHub.
/*
* qtip2 - Pretty powerful tooltips - v3.0.3 (modified for TISAS)
* http://qtip.com
*
* Copyright (c) 2016
* Released under the MIT licenses
* http://jquery.org/license
*
* Date: Wed May 11 2016 10:31 GMT+0100+0100
* Plugins: tips modal viewport svg imagemap ie6
* Styles: core basic css3
*/
.qtiptisas{
position: absolute;
left: -28000px;
top: -28000px;
display: none;
max-width: 280px;
min-width: 50px;
font-size: 10.5px;
line-height: 12px;
direction: ltr;
box-shadow: none;
padding: 0;
}
.qtiptisas-content{
position: relative;
padding: 5px 9px;
overflow: hidden;
text-align: left;
word-wrap: break-word;
}
.qtiptisas-titlebar{
position: relative;
padding: 5px 35px 5px 10px;
overflow: hidden;
border-width: 0 0 1px;
font-weight: bold;
}
.qtiptisas-titlebar + .qtiptisas-content{ border-top-width: 0 !important; }
/* Default close button class */
.qtiptisas-close{
position: absolute;
right: -9px; top: -9px;
z-index: 11; /* Overlap .qtiptisas-tip */
cursor: pointer;
outline: medium none;
border: 1px solid transparent;
}
.qtiptisas-titlebar .qtiptisas-close{
right: 4px; top: 50%;
margin-top: -9px;
}
* html .qtiptisas-titlebar .qtiptisas-close{ top: 16px; } /* IE fix */
.qtiptisas-titlebar .ui-icon,
.qtiptisas-icon .ui-icon{
display: block;
text-indent: -1000em;
direction: ltr;
}
.qtiptisas-icon, .qtiptisas-icon .ui-icon{
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
text-decoration: none;
}
.qtiptisas-icon .ui-icon{
width: 18px;
height: 14px;
line-height: 14px;
text-align: center;
text-indent: 0;
font: normal bold 10px/13px Tahoma,sans-serif;
color: inherit;
background: transparent none no-repeat -100em -100em;
}
/* Applied to 'focused' tooltips e.g. most recently displayed/interacted with */
.qtiptisas-focus{}
/* Applied on hover of tooltips i.e. added/removed on mouseenter/mouseleave respectively */
.qtiptisas-hover{}
/* Default tooltip style */
.qtiptisas-default{
border: 1px solid #F1D031;
background-color: #FFFFA3;
color: #555;
}
.qtiptisas-default .qtiptisas-titlebar{
background-color: #FFEF93;
}
.qtiptisas-default .qtiptisas-icon{
border-color: #CCC;
background: #F1F1F1;
color: #777;
}
.qtiptisas-default .qtiptisas-titlebar .qtiptisas-close{
border-color: #AAA;
color: #111;
}
/*! Light tooltip style */
.qtiptisas-light{
background-color: white;
border-color: #E2E2E2;
color: #454545;
}
.qtiptisas-light .qtiptisas-titlebar{
background-color: #f1f1f1;
}
/*! Dark tooltip style */
.qtiptisas-dark{
background-color: #505050;
border-color: #303030;
color: #f3f3f3;
}
.qtiptisas-dark .qtiptisas-titlebar{
background-color: #404040;
}
.qtiptisas-dark .qtiptisas-icon{
border-color: #444;
}
.qtiptisas-dark .qtiptisas-titlebar .ui-state-hover{
border-color: #303030;
}
/*! Cream tooltip style */
.qtiptisas-cream{
background-color: #FBF7AA;
border-color: #F9E98E;
color: #A27D35;
}
.qtiptisas-cream .qtiptisas-titlebar{
background-color: #F0DE7D;
}
.qtiptisas-cream .qtiptisas-close .qtiptisas-icon{
background-position: -82px 0;
}
/*! Red tooltip style */
.qtiptisas-red{
background-color: #F78B83;
border-color: #D95252;
color: #912323;
}
.qtiptisas-red .qtiptisas-titlebar{
background-color: #F06D65;
}
.qtiptisas-red .qtiptisas-close .qtiptisas-icon{
background-position: -102px 0;
}
.qtiptisas-red .qtiptisas-icon{
border-color: #D95252;
}
.qtiptisas-red .qtiptisas-titlebar .ui-state-hover{
border-color: #D95252;
}
/*! Green tooltip style */
.qtiptisas-green{
background-color: #CAED9E;
border-color: #90D93F;
color: #3F6219;
}
.qtiptisas-green .qtiptisas-titlebar{
background-color: #B0DE78;
}
.qtiptisas-green .qtiptisas-close .qtiptisas-icon{
background-position: -42px 0;
}
/*! Blue tooltip style */
.qtiptisas-blue{
background-color: #E5F6FE;
border-color: #ADD9ED;
color: #5E99BD;
}
.qtiptisas-blue .qtiptisas-titlebar{
background-color: #D0E9F5;
}
.qtiptisas-blue .qtiptisas-close .qtiptisas-icon{
background-position: -2px 0;
}
.qtiptisas-shadow{
-webkit-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);
-moz-box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);
box-shadow: 1px 1px 3px 1px rgba(0, 0, 0, 0.15);
}
/* Add rounded corners to your tooltips in: FF3+, Chrome 2+, Opera 10.6+, IE9+, Safari 2+ */
.qtiptisas-rounded,
.qtiptisas-tipsy,
.qtiptisas-bootstrap{
-moz-border-radius: 5px;
-webkit-border-radius: 5px;
border-radius: 5px;
}
.qtiptisas-rounded .qtiptisas-titlebar{
-moz-border-radius: 4px 4px 0 0;
-webkit-border-radius: 4px 4px 0 0;
border-radius: 4px 4px 0 0;
}
/* Youtube tooltip style */
.qtiptisas-youtube{
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
-webkit-box-shadow: 0 0 3px #333;
-moz-box-shadow: 0 0 3px #333;
box-shadow: 0 0 3px #333;
color: white;
border: 0 solid transparent;
background: #4A4A4A;
background-image: -webkit-gradient(linear,left top,left bottom,color-stop(0,#4A4A4A),color-stop(100%,black));
background-image: -webkit-linear-gradient(top,#4A4A4A 0,black 100%);
background-image: -moz-linear-gradient(top,#4A4A4A 0,black 100%);
background-image: -ms-linear-gradient(top,#4A4A4A 0,black 100%);
background-image: -o-linear-gradient(top,#4A4A4A 0,black 100%);
}
.qtiptisas-youtube .qtiptisas-titlebar{
background-color: #4A4A4A;
background-color: rgba(0,0,0,0);
}
.qtiptisas-youtube .qtiptisas-content{
padding: .75em;
font: 12px arial,sans-serif;
filter: progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);
-ms-filter: "progid:DXImageTransform.Microsoft.Gradient(GradientType=0,StartColorStr=#4a4a4a,EndColorStr=#000000);";
}
.qtiptisas-youtube .qtiptisas-icon{
border-color: #222;
}
.qtiptisas-youtube .qtiptisas-titlebar .ui-state-hover{
border-color: #303030;
}
/* jQuery TOOLS Tooltip style */
.qtiptisas-jtools{
background: #232323;
background: rgba(0, 0, 0, 0.7);
background-image: -webkit-gradient(linear, left top, left bottom, from(#717171), to(#232323));
background-image: -moz-linear-gradient(top, #717171, #232323);
background-image: -webkit-linear-gradient(top, #717171, #232323);
background-image: -ms-linear-gradient(top, #717171, #232323);
background-image: -o-linear-gradient(top, #717171, #232323);
border: 2px solid #ddd;
border: 2px solid rgba(241,241,241,1);
-moz-border-radius: 2px;
-webkit-border-radius: 2px;
border-radius: 2px;
-webkit-box-shadow: 0 0 12px #333;
-moz-box-shadow: 0 0 12px #333;
box-shadow: 0 0 12px #333;
}
/* IE Specific */
.qtiptisas-jtools .qtiptisas-titlebar{
background-color: transparent;
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A);
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#717171,endColorstr=#4A4A4A)";
}
.qtiptisas-jtools .qtiptisas-content{
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323);
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#4A4A4A,endColorstr=#232323)";
}
.qtiptisas-jtools .qtiptisas-titlebar,
.qtiptisas-jtools .qtiptisas-content{
background: transparent;
color: white;
border: 0 dashed transparent;
}
.qtiptisas-jtools .qtiptisas-icon{
border-color: #555;
}
.qtiptisas-jtools .qtiptisas-titlebar .ui-state-hover{
border-color: #333;
}
/* Cluetip style */
.qtiptisas-cluetip{
-webkit-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4);
-moz-box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4);
box-shadow: 4px 4px 5px rgba(0, 0, 0, 0.4);
background-color: #D9D9C2;
color: #111;
border: 0 dashed transparent;
}
.qtiptisas-cluetip .qtiptisas-titlebar{
background-color: #87876A;
color: white;
border: 0 dashed transparent;
}
.qtiptisas-cluetip .qtiptisas-icon{
border-color: #808064;
}
.qtiptisas-cluetip .qtiptisas-titlebar .ui-state-hover{
border-color: #696952;
color: #696952;
}
/* Tipsy style */
.qtiptisas-tipsy{
background: black;
background: rgba(0, 0, 0, .87);
color: white;
border: 0 solid transparent;
font-size: 11px;
font-family: 'Lucida Grande', sans-serif;
font-weight: bold;
line-height: 16px;
text-shadow: 0 1px black;
}
.qtiptisas-tipsy .qtiptisas-titlebar{
padding: 6px 35px 0 10px;
background-color: transparent;
}
.qtiptisas-tipsy .qtiptisas-content{
padding: 6px 10px;
}
.qtiptisas-tipsy .qtiptisas-icon{
border-color: #222;
text-shadow: none;
}
.qtiptisas-tipsy .qtiptisas-titlebar .ui-state-hover{
border-color: #303030;
}
/* Tipped style */
.qtiptisas-tipped{
border: 3px solid #959FA9;
-moz-border-radius: 3px;
-webkit-border-radius: 3px;
border-radius: 3px;
background-color: #F9F9F9;
color: #454545;
font-weight: normal;
font-family: serif;
}
.qtiptisas-tipped .qtiptisas-titlebar{
border-bottom-width: 0;
color: white;
background: #3A79B8;
background-image: -webkit-gradient(linear, left top, left bottom, from(#3A79B8), to(#2E629D));
background-image: -webkit-linear-gradient(top, #3A79B8, #2E629D);
background-image: -moz-linear-gradient(top, #3A79B8, #2E629D);
background-image: -ms-linear-gradient(top, #3A79B8, #2E629D);
background-image: -o-linear-gradient(top, #3A79B8, #2E629D);
filter:progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D);
-ms-filter: "progid:DXImageTransform.Microsoft.gradient(startColorstr=#3A79B8,endColorstr=#2E629D)";
}
.qtiptisas-tipped .qtiptisas-icon{
border: 2px solid #285589;
background: #285589;
}
.qtiptisas-tipped .qtiptisas-icon .ui-icon{
background-color: #FBFBFB;
color: #555;
}
/**
* Twitter Bootstrap style.
*
* Tested with IE 8, IE 9, Chrome 18, Firefox 9, Opera 11.
* Does not work with IE 7.
*/
.qtiptisas-bootstrap{
/** Taken from Bootstrap body */
font-size: 14px;
line-height: 20px;
color: #333333;
/** Taken from Bootstrap .popover */
padding: 1px;
background-color: #ffffff;
border: 1px solid #ccc;
border: 1px solid rgba(0, 0, 0, 0.2);
-webkit-border-radius: 6px;
-moz-border-radius: 6px;
border-radius: 6px;
-webkit-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-moz-box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
box-shadow: 0 5px 10px rgba(0, 0, 0, 0.2);
-webkit-background-clip: padding-box;
-moz-background-clip: padding;
background-clip: padding-box;
}
.qtiptisas-bootstrap .qtiptisas-titlebar{
/** Taken from Bootstrap .popover-title */
padding: 8px 14px;
margin: 0;
font-size: 14px;
font-weight: normal;
line-height: 18px;
background-color: #f7f7f7;
border-bottom: 1px solid #ebebeb;
-webkit-border-radius: 5px 5px 0 0;
-moz-border-radius: 5px 5px 0 0;
border-radius: 5px 5px 0 0;
}
.qtiptisas-bootstrap .qtiptisas-titlebar .qtiptisas-close{
/**
* Overrides qtiptisas2:
* .qtiptisas-titlebar .qtiptisas-close{
* [...]
* right: 4px;
* top: 50%;
* [...]
* border-style: solid;
* }
*/
right: 11px;
top: 45%;
border-style: none;
}
.qtiptisas-bootstrap .qtiptisas-content{
/** Taken from Bootstrap .popover-content */
padding: 9px 14px;
}
.qtiptisas-bootstrap .qtiptisas-icon{
/**
* Overrides qtiptisas2:
* .qtiptisas-default .qtiptisas-icon {
* border-color: #CCC;
* background: #F1F1F1;
* color: #777;
* }
*/
background: transparent;
}
.qtiptisas-bootstrap .qtiptisas-icon .ui-icon{
/**
* Overrides qtiptisas2:
* .qtiptisas-icon .ui-icon{
* width: 18px;
* height: 14px;
* }
*/
width: auto;
height: auto;
/* Taken from Bootstrap .close */
float: right;
font-size: 20px;
font-weight: bold;
line-height: 18px;
color: #000000;
text-shadow: 0 1px 0 #ffffff;
opacity: 0.2;
filter: alpha(opacity=20);
}
.qtiptisas-bootstrap .qtiptisas-icon .ui-icon:hover{
/* Taken from Bootstrap .close:hover */
color: #000000;
text-decoration: none;
cursor: pointer;
opacity: 0.4;
filter: alpha(opacity=40);
}
/* IE9 fix - removes all filters */
.qtiptisas:not(.ie9haxors) div.qtiptisas-content,
.qtiptisas:not(.ie9haxors) div.qtiptisas-titlebar{
filter: none;
-ms-filter: none;
}
.qtiptisas .qtiptisas-tip{
margin: 0 auto;
overflow: hidden;
z-index: 10;
}
/* Opera bug #357 - Incorrect tip position
https://github.com/Craga89/qtip2/issues/367 */
x:-o-prefocus, .qtiptisas .qtiptisas-tip{
visibility: hidden;
}
.qtiptisas .qtiptisas-tip,
.qtiptisas .qtiptisas-tip .qtiptisas-vml,
.qtiptisas .qtiptisas-tip canvas{
position: absolute;
color: #123456;
background: transparent;
border: 0 dashed transparent;
}
.qtiptisas .qtiptisas-tip canvas{ top: 0; left: 0; }
.qtiptisas .qtiptisas-tip .qtiptisas-vml{
behavior: url(#default#VML);
display: inline-block;
visibility: visible;
}
#qtiptisas-overlay{
position: fixed;
left: 0; top: 0;
width: 100%; height: 100%;
}
/* Applied to modals with show.modal.blur set to true */
#qtiptisas-overlay.blurs{ cursor: pointer; }
/* Change opacity of overlay here */
#qtiptisas-overlay div{
position: absolute;
left: 0; top: 0;
width: 100%; height: 100%;
background-color: black;
opacity: 0.7;
filter:alpha(opacity=70);
-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=70)";
}
.qtiptisasmodal-ie6fix{
position: absolute !important;
}
/*
* qTip2 - Pretty powerful tooltips - v3.0.3 (modified for TISAS)
* http://qtip2.com
*
* Copyright (c) 2016
* Released under the MIT licenses
* http://jquery.org/license
*
* Date: Wed May 11 2016 10:31 GMT+0100+0100
* Plugins: tips modal viewport svg imagemap ie6
* Styles: core basic css3
*/
/*global window: false, jQuery: false, console: false, define: false */
/* Cache window, document, undefined */
(function( window, document, undefined ) {
// Uses AMD or browser globals to create a jQuery plugin.
(function( factory ) {
"use strict";
if(typeof define === 'function' && define.amd) {
define(['jquery'], factory);
}
else if(jQuery && !jQuery.fn.qtiptisas) {
factory(jQuery);
}
}
(function($) {
"use strict"; // Enable ECMAScript "strict" operation for this function. See more: http://ejohn.org/blog/ecmascript-5-strict-mode-json-and-more/
;// Munge the primitives - Paul Irish tip
var TRUE = true,
FALSE = false,
NULL = null,
// Common variables
X = 'x', Y = 'y',
WIDTH = 'width',
HEIGHT = 'height',
// Positioning sides
TOP = 'top',
LEFT = 'left',
BOTTOM = 'bottom',
RIGHT = 'right',
CENTER = 'center',
// Position adjustment types
FLIP = 'flip',
FLIPINVERT = 'flipinvert',
SHIFT = 'shift',
// Shortcut vars
QTIPTISAS, PROTOTYPE, CORNER, CHECKS,
PLUGINS = {},
NAMESPACE = 'qtiptisas',
ATTR_HAS = 'data-hasqtiptisas',
ATTR_ID = 'data-qtiptisas-id',
WIDGET = ['ui-widget', 'ui-tooltip'],
SELECTOR = '.'+NAMESPACE,
INACTIVE_EVENTS = 'click dblclick mousedown mouseup mousemove mouseleave mouseenter'.split(' '),
CLASS_FIXED = NAMESPACE+'-fixed',
CLASS_DEFAULT = NAMESPACE + '-default',
CLASS_FOCUS = NAMESPACE + '-focus',
CLASS_HOVER = NAMESPACE + '-hover',
CLASS_DISABLED = NAMESPACE+'-disabled',
replaceSuffix = '_replacedByqTipTisas',
oldtitle = 'oldtitle',
trackingBound,
// Browser detection
BROWSER = {
/*
* IE version detection
*
* Adapted from: http://ajaxian.com/archives/attack-of-the-ie-conditional-comment
* Credit to James Padolsey for the original implemntation!
*/
ie: (function() {
/* eslint-disable no-empty */
var v, i;
for (
v = 4, i = document.createElement('div');
(i.innerHTML = '<!--[if gt IE ' + v + ']><i></i><![endif]-->') && i.getElementsByTagName('i')[0];
v+=1
) {}
return v > 4 ? v : NaN;
/* eslint-enable no-empty */
})(),
/*
* iOS version detection
*/
iOS: parseFloat(
('' + (/CPU.*OS ([0-9_]{1,5})|(CPU like).*AppleWebKit.*Mobile/i.exec(navigator.userAgent) || [0,''])[1])
.replace('undefined', '3_2').replace('_', '.').replace('_', '')
) || FALSE
};
;function QTipTisas(target, options, id, attr) {
// Elements and ID
this.id = id;
this.target = target;
this.tooltip = NULL;
this.elements = { target: target };
// Internal constructs
this._id = NAMESPACE + '-' + id;
this.timers = { img: {} };
this.options = options;
this.plugins = {};
// Cache object
this.cache = {
event: {},
target: $(),
disabled: FALSE,
attr: attr,
onTooltip: FALSE,
lastClass: ''
};
// Set the initial flags
this.rendered = this.destroyed = this.disabled = this.waiting =
this.hiddenDuringWait = this.positioning = this.triggering = FALSE;
}
PROTOTYPE = QTipTisas.prototype;
PROTOTYPE._when = function(deferreds) {
return $.when.apply($, deferreds);
};
PROTOTYPE.render = function(show) {
if(this.rendered || this.destroyed) { return this; } // If tooltip has already been rendered, exit
var self = this,
options = this.options,
cache = this.cache,
elements = this.elements,
text = options.content.text,
title = options.content.title,
button = options.content.button,
posOptions = options.position,
deferreds = [];
// Add ARIA attributes to target
$.attr(this.target[0], 'aria-describedby', this._id);
// Create public position object that tracks current position corners
cache.posClass = this._createPosClass(
(this.position = { my: posOptions.my, at: posOptions.at }).my
);
// Create tooltip element
this.tooltip = elements.tooltip = $('<div/>', {
'id': this._id,
'class': [ NAMESPACE, CLASS_DEFAULT, options.style.classes, cache.posClass ].join(' '),
'width': options.style.width || '',
'height': options.style.height || '',
'tracking': posOptions.target === 'mouse' && posOptions.adjust.mouse,
/* ARIA specific attributes */
'role': 'alert',
'aria-live': 'polite',
'aria-atomic': FALSE,
'aria-describedby': this._id + '-content',
'aria-hidden': TRUE
})
.toggleClass(CLASS_DISABLED, this.disabled)
.attr(ATTR_ID, this.id)
.data(NAMESPACE, this)
.appendTo(posOptions.container)
.append(
// Create content element
elements.content = $('<div />', {
'class': NAMESPACE + '-content',
'id': this._id + '-content',
'aria-atomic': TRUE
})
);
// Set rendered flag and prevent redundant reposition calls for now
this.rendered = -1;
this.positioning = TRUE;
// Create title...
if(title) {
this._createTitle();
// Update title only if its not a callback (called in toggle if so)
if(!$.isFunction(title)) {
deferreds.push( this._updateTitle(title, FALSE) );
}
}
// Create button
if(button) { this._createButton(); }
// Set proper rendered flag and update content if not a callback function (called in toggle)
if(!$.isFunction(text)) {
deferreds.push( this._updateContent(text, FALSE) );
}
this.rendered = TRUE;
// Setup widget classes
this._setWidget();
// Initialize 'render' plugins
$.each(PLUGINS, function(name) {
var instance;
if(this.initialize === 'render' && (instance = this(self))) {
self.plugins[name] = instance;
}
});
// Unassign initial events and assign proper events
this._unassignEvents();
this._assignEvents();
// When deferreds have completed
this._when(deferreds).then(function() {
// tooltiprender event
self._trigger('render');
// Reset flags
self.positioning = FALSE;
// Show tooltip if not hidden during wait period
if(!self.hiddenDuringWait && (options.show.ready || show)) {
self.toggle(TRUE, cache.event, FALSE);
}
self.hiddenDuringWait = FALSE;
});
// Expose API
QTIPTISAS.api[this.id] = this;
return this;
};
PROTOTYPE.destroy = function(immediate) {
// Set flag the signify destroy is taking place to plugins
// and ensure it only gets destroyed once!
if(this.destroyed) { return this.target; }
function process() {
if(this.destroyed) { return; }
this.destroyed = TRUE;
var target = this.target,
title = target.attr(oldtitle),
timer;
// Destroy tooltip if rendered
if(this.rendered) {
this.tooltip.stop(1,0).find('*').remove().end().remove();
}
// Destroy all plugins
$.each(this.plugins, function() {
this.destroy && this.destroy();
});
// Clear timers
for (timer in this.timers) {
if (this.timers.hasOwnProperty(timer)) {
clearTimeout(this.timers[timer]);
}
}
// Remove api object and ARIA attributes
target.removeData(NAMESPACE)
.removeAttr(ATTR_ID)
.removeAttr(ATTR_HAS)
.removeAttr('aria-describedby');
// Reset old title attribute if removed
if(this.options.suppress && title) {
target.attr('title', title).removeAttr(oldtitle);
}
// Remove qTipTisas events associated with this API
this._unassignEvents();
// Remove ID from used id objects, and delete object references
// for better garbage collection and leak protection
this.options = this.elements = this.cache = this.timers =
this.plugins = this.mouse = NULL;
// Delete epoxsed API object
delete QTIPTISAS.api[this.id];
}
// If an immediate destroy is needed
if((immediate !== TRUE || this.triggering === 'hide') && this.rendered) {
this.tooltip.one('tooltiphidden', $.proxy(process, this));
!this.triggering && this.hide();
}
// If we're not in the process of hiding... process
else { process.call(this); }
return this.target;
};
;function invalidOpt(a) {
return a === NULL || $.type(a) !== 'object';
}
function invalidContent(c) {
return !($.isFunction(c) ||
c && c.attr ||
c.length ||
$.type(c) === 'object' && (c.jquery || c.then));
}
// Option object sanitizer
function sanitizeOptions(opts) {
var content, text, ajax, once;
if(invalidOpt(opts)) { return FALSE; }
if(invalidOpt(opts.metadata)) {
opts.metadata = { type: opts.metadata };
}
if('content' in opts) {
content = opts.content;
if(invalidOpt(content) || content.jquery || content.done) {
text = invalidContent(content) ? FALSE : content;
content = opts.content = {
text: text
};
}
else { text = content.text; }
// DEPRECATED - Old content.ajax plugin functionality
// Converts it into the proper Deferred syntax
if('ajax' in content) {
ajax = content.ajax;
once = ajax && ajax.once !== FALSE;
delete content.ajax;
content.text = function(event, api) {
var loading = text || $(this).attr(api.options.content.attr) || 'Loading...',
deferred = $.ajax(
$.extend({}, ajax, { context: api })
)
.then(ajax.success, NULL, ajax.error)
.then(function(newContent) {
if(newContent && once) { api.set('content.text', newContent); }
return newContent;
},
function(xhr, status, error) {
if(api.destroyed || xhr.status === 0) { return; }
api.set('content.text', status + ': ' + error);
});
return !once ? (api.set('content.text', loading), deferred) : loading;
};
}
if('title' in content) {
if($.isPlainObject(content.title)) {
content.button = content.title.button;
content.title = content.title.text;
}
if(invalidContent(content.title || FALSE)) {
content.title = FALSE;
}
}
}
if('position' in opts && invalidOpt(opts.position)) {
opts.position = { my: opts.position, at: opts.position };
}
if('show' in opts && invalidOpt(opts.show)) {
opts.show = opts.show.jquery ? { target: opts.show } :
opts.show === TRUE ? { ready: TRUE } : { event: opts.show };
}
if('hide' in opts && invalidOpt(opts.hide)) {
opts.hide = opts.hide.jquery ? { target: opts.hide } : { event: opts.hide };
}
if('style' in opts && invalidOpt(opts.style)) {
opts.style = { classes: opts.style };
}
// Sanitize plugin options
$.each(PLUGINS, function() {
this.sanitize && this.sanitize(opts);
});
return opts;
}
// Setup builtin .set() option checks
CHECKS = PROTOTYPE.checks = {
builtin: {
// Core checks
'^id$': function(obj, o, v, prev) {
var id = v === TRUE ? QTIPTISAS.nextid : v,
newId = NAMESPACE + '-' + id;
if(id !== FALSE && id.length > 0 && !$('#'+newId).length) {
this._id = newId;
if(this.rendered) {
this.tooltip[0].id = this._id;
this.elements.content[0].id = this._id + '-content';
this.elements.title[0].id = this._id + '-title';
}
}
else { obj[o] = prev; }
},
'^prerender': function(obj, o, v) {
v && !this.rendered && this.render(this.options.show.ready);
},
// Content checks
'^content.text$': function(obj, o, v) {
this._updateContent(v);
},
'^content.attr$': function(obj, o, v, prev) {
if(this.options.content.text === this.target.attr(prev)) {
this._updateContent( this.target.attr(v) );
}
},
'^content.title$': function(obj, o, v) {
// Remove title if content is null
if(!v) { return this._removeTitle(); }
// If title isn't already created, create it now and update
v && !this.elements.title && this._createTitle();
this._updateTitle(v);
},
'^content.button$': function(obj, o, v) {
this._updateButton(v);
},
'^content.title.(text|button)$': function(obj, o, v) {
this.set('content.'+o, v); // Backwards title.text/button compat
},
// Position checks
'^position.(my|at)$': function(obj, o, v){
if('string' === typeof v) {
this.position[o] = obj[o] = new CORNER(v, o === 'at');
}
},
'^position.container$': function(obj, o, v){
this.rendered && this.tooltip.appendTo(v);
},
// Show checks
'^show.ready$': function(obj, o, v) {
v && (!this.rendered && this.render(TRUE) || this.toggle(TRUE));
},
// Style checks
'^style.classes$': function(obj, o, v, p) {
this.rendered && this.tooltip.removeClass(p).addClass(v);
},
'^style.(width|height)': function(obj, o, v) {
this.rendered && this.tooltip.css(o, v);
},
'^style.widget|content.title': function() {
this.rendered && this._setWidget();
},
'^style.def': function(obj, o, v) {
this.rendered && this.tooltip.toggleClass(CLASS_DEFAULT, !!v);
},
// Events check
'^events.(render|show|move|hide|focus|blur)$': function(obj, o, v) {
this.rendered && this.tooltip[($.isFunction(v) ? '' : 'un') + 'bind']('tooltip'+o, v);
},
// Properties which require event reassignment
'^(show|hide|position).(event|target|fixed|inactive|leave|distance|viewport|adjust)': function() {
if(!this.rendered) { return; }
// Set tracking flag
var posOptions = this.options.position;
this.tooltip.attr('tracking', posOptions.target === 'mouse' && posOptions.adjust.mouse);
// Reassign events
this._unassignEvents();
this._assignEvents();
}
}
};
// Dot notation converter
function convertNotation(options, notation) {
var i = 0, obj, option = options,
// Split notation into array
levels = notation.split('.');
// Loop through
while(option = option[ levels[i++] ]) {
if(i < levels.length) { obj = option; }
}
return [obj || options, levels.pop()];
}
PROTOTYPE.get = function(notation) {
if(this.destroyed) { return this; }
var o = convertNotation(this.options, notation.toLowerCase()),
result = o[0][ o[1] ];
return result.precedance ? result.string() : result;
};
function setCallback(notation, args) {
var category, rule, match;
for(category in this.checks) {
if (!this.checks.hasOwnProperty(category)) { continue; }
for(rule in this.checks[category]) {
if (!this.checks[category].hasOwnProperty(rule)) { continue; }
if(match = (new RegExp(rule, 'i')).exec(notation)) {
args.push(match);
if(category === 'builtin' || this.plugins[category]) {
this.checks[category][rule].apply(
this.plugins[category] || this, args
);
}
}
}
}
}
var rmove = /^position\.(my|at|adjust|target|container|viewport)|style|content|show\.ready/i,
rrender = /^prerender|show\.ready/i;
PROTOTYPE.set = function(option, value) {
if(this.destroyed) { return this; }
var rendered = this.rendered,
reposition = FALSE,
options = this.options,
name;
// Convert singular option/value pair into object form
if('string' === typeof option) {
name = option; option = {}; option[name] = value;
}
else { option = $.extend({}, option); }
// Set all of the defined options to their new values
$.each(option, function(notation, val) {
if(rendered && rrender.test(notation)) {
delete option[notation]; return;
}
// Set new obj value
var obj = convertNotation(options, notation.toLowerCase()), previous;
previous = obj[0][ obj[1] ];
obj[0][ obj[1] ] = val && val.nodeType ? $(val) : val;
// Also check if we need to reposition
reposition = rmove.test(notation) || reposition;
// Set the new params for the callback
option[notation] = [obj[0], obj[1], val, previous];
});
// Re-sanitize options
sanitizeOptions(options);
/*
* Execute any valid callbacks for the set options
* Also set positioning flag so we don't get loads of redundant repositioning calls.
*/
this.positioning = TRUE;
$.each(option, $.proxy(setCallback, this));
this.positioning = FALSE;
// Update position if needed
if(this.rendered && this.tooltip[0].offsetWidth > 0 && reposition) {
this.reposition( options.position.target === 'mouse' ? NULL : this.cache.event );
}
return this;
};
;PROTOTYPE._update = function(content, element) {
var self = this,
cache = this.cache;
// Make sure tooltip is rendered and content is defined. If not return
if(!this.rendered || !content) { return FALSE; }
// Use function to parse content
if($.isFunction(content)) {
content = content.call(this.elements.target, cache.event, this) || '';
}
// Handle deferred content
if($.isFunction(content.then)) {
cache.waiting = TRUE;
return content.then(function(c) {
cache.waiting = FALSE;
return self._update(c, element);
}, NULL, function(e) {
return self._update(e, element);
});
}
// If content is null... return false
if(content === FALSE || !content && content !== '') { return FALSE; }
// Append new content if its a DOM array and show it if hidden
if(content.jquery && content.length > 0) {
element.empty().append(
content.css({ display: 'block', visibility: 'visible' })
);
}
// Content is a regular string, insert the new content
else { element.html(content); }
// Wait for content to be loaded, and reposition
return this._waitForContent(element).then(function(images) {
if(self.rendered && self.tooltip[0].offsetWidth > 0) {
self.reposition(cache.event, !images.length);
}
});
};
PROTOTYPE._waitForContent = function(element) {
var cache = this.cache;
// Set flag
cache.waiting = TRUE;
// If imagesLoaded is included, ensure images have loaded and return promise
return ( $.fn.imagesLoaded ? element.imagesLoaded() : new $.Deferred().resolve([]) )
.done(function() { cache.waiting = FALSE; })
.promise();
};
PROTOTYPE._updateContent = function(content, reposition) {
this._update(content, this.elements.content, reposition);
};
PROTOTYPE._updateTitle = function(content, reposition) {
if(this._update(content, this.elements.title, reposition) === FALSE) {
this._removeTitle(FALSE);
}
};
PROTOTYPE._createTitle = function()
{
var elements = this.elements,
id = this._id+'-title';
// Destroy previous title element, if present
if(elements.titlebar) { this._removeTitle(); }
// Create title bar and title elements
elements.titlebar = $('<div />', {
'class': NAMESPACE + '-titlebar ' + (this.options.style.widget ? createWidgetClass('header') : '')
})
.append(
elements.title = $('<div />', {
'id': id,
'class': NAMESPACE + '-title',
'aria-atomic': TRUE
})
)
.insertBefore(elements.content)
// Button-specific events
.delegate('.qtiptisas-close', 'mousedown keydown mouseup keyup mouseout', function(event) {
$(this).toggleClass('ui-state-active ui-state-focus', event.type.substr(-4) === 'down');
})
.delegate('.qtiptisas-close', 'mouseover mouseout', function(event){
$(this).toggleClass('ui-state-hover', event.type === 'mouseover');
});
// Create button if enabled
if(this.options.content.button) { this._createButton(); }
};
PROTOTYPE._removeTitle = function(reposition)
{
var elements = this.elements;
if(elements.title) {
elements.titlebar.remove();
elements.titlebar = elements.title = elements.button = NULL;
// Reposition if enabled
if(reposition !== FALSE) { this.reposition(); }
}
};
;PROTOTYPE._createPosClass = function(my) {
return NAMESPACE + '-pos-' + (my || this.options.position.my).abbrev();
};
PROTOTYPE.reposition = function(event, effect) {
if(!this.rendered || this.positioning || this.destroyed) { return this; }
// Set positioning flag
this.positioning = TRUE;
var cache = this.cache,
tooltip = this.tooltip,
posOptions = this.options.position,
target = posOptions.target,
my = posOptions.my,
at = posOptions.at,
viewport = posOptions.viewport,
container = posOptions.container,
adjust = posOptions.adjust,
method = adjust.method.split(' '),
tooltipWidth = tooltip.outerWidth(FALSE),
tooltipHeight = tooltip.outerHeight(FALSE),
targetWidth = 0,
targetHeight = 0,
type = tooltip.css('position'),
position = { left: 0, top: 0 },
visible = tooltip[0].offsetWidth > 0,
isScroll = event && event.type === 'scroll',
win = $(window),
doc = container[0].ownerDocument,
mouse = this.mouse,
pluginCalculations, offset, adjusted, newClass;
// Check if absolute position was passed
if($.isArray(target) && target.length === 2) {
// Force left top and set position
at = { x: LEFT, y: TOP };
position = { left: target[0], top: target[1] };
}
// Check if mouse was the target
else if(target === 'mouse') {
// Force left top to allow flipping
at = { x: LEFT, y: TOP };
// Use the mouse origin that caused the show event, if distance hiding is enabled
if((!adjust.mouse || this.options.hide.distance) && cache.origin && cache.origin.pageX) {
event = cache.origin;
}
// Use cached event for resize/scroll events
else if(!event || event && (event.type === 'resize' || event.type === 'scroll')) {
event = cache.event;
}
// Otherwise, use the cached mouse coordinates if available
else if(mouse && mouse.pageX) {
event = mouse;
}
// Calculate body and container offset and take them into account below
if(type !== 'static') { position = container.offset(); }
if(doc.body.offsetWidth !== (window.innerWidth || doc.documentElement.clientWidth)) {
offset = $(document.body).offset();
}
// Use event coordinates for position
position = {
left: event.pageX - position.left + (offset && offset.left || 0),
top: event.pageY - position.top + (offset && offset.top || 0)
};
// Scroll events are a pain, some browsers
if(adjust.mouse && isScroll && mouse) {
position.left -= (mouse.scrollX || 0) - win.scrollLeft();
position.top -= (mouse.scrollY || 0) - win.scrollTop();
}
}
// Target wasn't mouse or absolute...
else {
// Check if event targetting is being used
if(target === 'event') {
if(event && event.target && event.type !== 'scroll' && event.type !== 'resize') {
cache.target = $(event.target);
}
else if(!event.target) {
cache.target = this.elements.target;
}
}
else if(target !== 'event'){
cache.target = $(target.jquery ? target : this.elements.target);
}
target = cache.target;
// Parse the target into a jQuery object and make sure there's an element present
target = $(target).eq(0);
if(target.length === 0) { return this; }
// Check if window or document is the target
else if(target[0] === document || target[0] === window) {
targetWidth = BROWSER.iOS ? window.innerWidth : target.width();
targetHeight = BROWSER.iOS ? window.innerHeight : target.height();
if(target[0] === window) {
position = {
top: (viewport || target).scrollTop(),
left: (viewport || target).scrollLeft()
};
}
}
// Check if the target is an <AREA> element
else if(PLUGINS.imagemap && target.is('area')) {
pluginCalculations = PLUGINS.imagemap(this, target, at, PLUGINS.viewport ? method : FALSE);
}
// Check if the target is an SVG element
else if(PLUGINS.svg && target && target[0].ownerSVGElement) {
pluginCalculations = PLUGINS.svg(this, target, at, PLUGINS.viewport ? method : FALSE);
}
// Otherwise use regular jQuery methods
else {
targetWidth = target.outerWidth(FALSE);
targetHeight = target.outerHeight(FALSE);
position = target.offset();
}
// Parse returned plugin values into proper variables
if(pluginCalculations) {
targetWidth = pluginCalculations.width;
targetHeight = pluginCalculations.height;
offset = pluginCalculations.offset;
position = pluginCalculations.position;
}
// Adjust position to take into account offset parents
position = this.reposition.offset(target, position, container);
// Adjust for position.fixed tooltips (and also iOS scroll bug in v3.2-4.0 & v4.3-4.3.2)
if(BROWSER.iOS > 3.1 && BROWSER.iOS < 4.1 ||
BROWSER.iOS >= 4.3 && BROWSER.iOS < 4.33 ||
!BROWSER.iOS && type === 'fixed'
){
position.left -= win.scrollLeft();
position.top -= win.scrollTop();
}
// Adjust position relative to target
if(!pluginCalculations || pluginCalculations && pluginCalculations.adjustable !== FALSE) {
position.left += at.x === RIGHT ? targetWidth : at.x === CENTER ? targetWidth / 2 : 0;
position.top += at.y === BOTTOM ? targetHeight : at.y === CENTER ? targetHeight / 2 : 0;
}
}
// Adjust position relative to tooltip
position.left += adjust.x + (my.x === RIGHT ? -tooltipWidth : my.x === CENTER ? -tooltipWidth / 2 : 0);
position.top += adjust.y + (my.y === BOTTOM ? -tooltipHeight : my.y === CENTER ? -tooltipHeight / 2 : 0);
// Use viewport adjustment plugin if enabled
if(PLUGINS.viewport) {
adjusted = position.adjusted = PLUGINS.viewport(
this, position, posOptions, targetWidth, targetHeight, tooltipWidth, tooltipHeight
);
// Apply offsets supplied by positioning plugin (if used)
if(offset && adjusted.left) { position.left += offset.left; }
if(offset && adjusted.top) { position.top += offset.top; }
// Apply any new 'my' position
if(adjusted.my) { this.position.my = adjusted.my; }
}
// Viewport adjustment is disabled, set values to zero
else { position.adjusted = { left: 0, top: 0 }; }
// Set tooltip position class if it's changed
if(cache.posClass !== (newClass = this._createPosClass(this.position.my))) {
cache.posClass = newClass;
tooltip.removeClass(cache.posClass).addClass(newClass);
}
// tooltipmove event
if(!this._trigger('move', [position, viewport.elem || viewport], event)) { return this; }
delete position.adjusted;
// If effect is disabled, target it mouse, no animation is defined or positioning gives NaN out, set CSS directly
if(effect === FALSE || !visible || isNaN(position.left) || isNaN(position.top) || target === 'mouse' || !$.isFunction(posOptions.effect)) {
tooltip.css(position);
}
// Use custom function if provided
else if($.isFunction(posOptions.effect)) {
posOptions.effect.call(tooltip, this, $.extend({}, position));
tooltip.queue(function(next) {
// Reset attributes to avoid cross-browser rendering bugs
$(this).css({ opacity: '', height: '' });
if(BROWSER.ie) { this.style.removeAttribute('filter'); }
next();
});
}
// Set positioning flag
this.positioning = FALSE;
return this;
};
// Custom (more correct for qTipTisas!) offset calculator
PROTOTYPE.reposition.offset = function(elem, pos, container) {
if(!container[0]) { return pos; }
var ownerDocument = $(elem[0].ownerDocument),
quirks = !!BROWSER.ie && document.compatMode !== 'CSS1Compat',
parent = container[0],
scrolled, position, parentOffset, overflow;
function scroll(e, i) {
pos.left += i * e.scrollLeft();
pos.top += i * e.scrollTop();
}
// Compensate for non-static containers offset
do {
if((position = $.css(parent, 'position')) !== 'static') {
if(position === 'fixed') {
parentOffset = parent.getBoundingClientRect();
scroll(ownerDocument, -1);
}
else {
parentOffset = $(parent).position();
parentOffset.left += parseFloat($.css(parent, 'borderLeftWidth')) || 0;
parentOffset.top += parseFloat($.css(parent, 'borderTopWidth')) || 0;
}
pos.left -= parentOffset.left + (parseFloat($.css(parent, 'marginLeft')) || 0);
pos.top -= parentOffset.top + (parseFloat($.css(parent, 'marginTop')) || 0);
// If this is the first parent element with an overflow of "scroll" or "auto", store it
if(!scrolled && (overflow = $.css(parent, 'overflow')) !== 'hidden' && overflow !== 'visible') { scrolled = $(parent); }
}
}
while(parent = parent.offsetParent);
// Compensate for containers scroll if it also has an offsetParent (or in IE quirks mode)
if(scrolled && (scrolled[0] !== ownerDocument[0] || quirks)) {
scroll(scrolled, 1);
}
return pos;
};
// Corner class
var C = (CORNER = PROTOTYPE.reposition.Corner = function(corner, forceY) {
corner = ('' + corner).replace(/([A-Z])/, ' $1').replace(/middle/gi, CENTER).toLowerCase();
this.x = (corner.match(/left|right/i) || corner.match(/center/) || ['inherit'])[0].toLowerCase();
this.y = (corner.match(/top|bottom|center/i) || ['inherit'])[0].toLowerCase();
this.forceY = !!forceY;
var f = corner.charAt(0);
this.precedance = f === 't' || f === 'b' ? Y : X;
}).prototype;
C.invert = function(z, center) {
this[z] = this[z] === LEFT ? RIGHT : this[z] === RIGHT ? LEFT : center || this[z];
};
C.string = function(join) {
var x = this.x, y = this.y;
var result = x !== y ?
x === 'center' || y !== 'center' && (this.precedance === Y || this.forceY) ?
[y,x] :
[x,y] :
[x];
return join !== false ? result.join(' ') : result;
};
C.abbrev = function() {
var result = this.string(false);
return result[0].charAt(0) + (result[1] && result[1].charAt(0) || '');
};
C.clone = function() {
return new CORNER( this.string(), this.forceY );
};
;
PROTOTYPE.toggle = function(state, event) {
var cache = this.cache,
options = this.options,
tooltip = this.tooltip;
// Try to prevent flickering when tooltip overlaps show element
if(event) {
if((/over|enter/).test(event.type) && cache.event && (/out|leave/).test(cache.event.type) &&
options.show.target.add(event.target).length === options.show.target.length &&
tooltip.has(event.relatedTarget).length) {
return this;
}
// Cache event
cache.event = $.event.fix(event);
}
// If we're currently waiting and we've just hidden... stop it
this.waiting && !state && (this.hiddenDuringWait = TRUE);
// Render the tooltip if showing and it isn't already
if(!this.rendered) { return state ? this.render(1) : this; }
else if(this.destroyed || this.disabled) { return this; }
var type = state ? 'show' : 'hide',
opts = this.options[type],
posOptions = this.options.position,
contentOptions = this.options.content,
width = this.tooltip.css('width'),
visible = this.tooltip.is(':visible'),
animate = state || opts.target.length === 1,
sameTarget = !event || opts.target.length < 2 || cache.target[0] === event.target,
identicalState, allow, after;
// Detect state if valid one isn't provided
if((typeof state).search('boolean|number')) { state = !visible; }
// Check if the tooltip is in an identical state to the new would-be state
identicalState = !tooltip.is(':animated') && visible === state && sameTarget;
// Fire tooltip(show/hide) event and check if destroyed
allow = !identicalState ? !!this._trigger(type, [90]) : NULL;
// Check to make sure the tooltip wasn't destroyed in the callback
if(this.destroyed) { return this; }
// If the user didn't stop the method prematurely and we're showing the tooltip, focus it
if(allow !== FALSE && state) { this.focus(event); }
// If the state hasn't changed or the user stopped it, return early
if(!allow || identicalState) { return this; }
// Set ARIA hidden attribute
$.attr(tooltip[0], 'aria-hidden', !!!state);
// Execute state specific properties
if(state) {
// Store show origin coordinates
this.mouse && (cache.origin = $.event.fix(this.mouse));
// Update tooltip content & title if it's a dynamic function
if($.isFunction(contentOptions.text)) { this._updateContent(contentOptions.text, FALSE); }
if($.isFunction(contentOptions.title)) { this._updateTitle(contentOptions.title, FALSE); }
// Cache mousemove events for positioning purposes (if not already tracking)
if(!trackingBound && posOptions.target === 'mouse' && posOptions.adjust.mouse) {
$(document).bind('mousemove.'+NAMESPACE, this._storeMouse);
trackingBound = TRUE;
}
// Update the tooltip position (set width first to prevent viewport/max-width issues)
if(!width) { tooltip.css('width', tooltip.outerWidth(FALSE)); }
this.reposition(event, arguments[2]);
if(!width) { tooltip.css('width', ''); }
// Hide other tooltips if tooltip is solo
if(!!opts.solo) {
(typeof opts.solo === 'string' ? $(opts.solo) : $(SELECTOR, opts.solo))
.not(tooltip).not(opts.target).qtiptisas('hide', new $.Event('tooltipsolo'));
}
}
else {
// Clear show timer if we're hiding
clearTimeout(this.timers.show);
// Remove cached origin on hide
delete cache.origin;
// Remove mouse tracking event if not needed (all tracking qTipTisass are hidden)
if(trackingBound && !$(SELECTOR+'[tracking="true"]:visible', opts.solo).not(tooltip).length) {
$(document).unbind('mousemove.'+NAMESPACE);
trackingBound = FALSE;
}
// Blur the tooltip
this.blur(event);
}
// Define post-animation, state specific properties
after = $.proxy(function() {
if(state) {
// Prevent antialias from disappearing in IE by removing filter
if(BROWSER.ie) { tooltip[0].style.removeAttribute('filter'); }
// Remove overflow setting to prevent tip bugs
tooltip.css('overflow', '');
// Autofocus elements if enabled
if('string' === typeof opts.autofocus) {
$(this.options.show.autofocus, tooltip).focus();
}
// If set, hide tooltip when inactive for delay period
this.options.show.target.trigger('qtiptisas-'+this.id+'-inactive');
}
else {
// Reset CSS states
tooltip.css({
display: '',
visibility: '',
opacity: '',
left: '',
top: ''
});
}
// tooltipvisible/tooltiphidden events
this._trigger(state ? 'visible' : 'hidden');
}, this);
// If no effect type is supplied, use a simple toggle
if(opts.effect === FALSE || animate === FALSE) {
tooltip[ type ]();
after();
}
// Use custom function if provided
else if($.isFunction(opts.effect)) {
tooltip.stop(1, 1);
opts.effect.call(tooltip, this);
tooltip.queue('fx', function(n) {
after(); n();
});
}
// Use basic fade function by default
else { tooltip.fadeTo(90, state ? 1 : 0, after); }
// If inactive hide method is set, active it
if(state) { opts.target.trigger('qtiptisas-'+this.id+'-inactive'); }
return this;
};
PROTOTYPE.show = function(event) { return this.toggle(TRUE, event); };
PROTOTYPE.hide = function(event) { return this.toggle(FALSE, event); };
;PROTOTYPE.focus = function(event) {
if(!this.rendered || this.destroyed) { return this; }
var qtiptisass = $(SELECTOR),
tooltip = this.tooltip,
curIndex = parseInt(tooltip[0].style.zIndex, 10),
newIndex = QTIPTISAS.zindex + qtiptisass.length;
// Only update the z-index if it has changed and tooltip is not already focused
if(!tooltip.hasClass(CLASS_FOCUS)) {
// tooltipfocus event
if(this._trigger('focus', [newIndex], event)) {
// Only update z-index's if they've changed
if(curIndex !== newIndex) {
// Reduce our z-index's and keep them properly ordered
qtiptisass.each(function() {
if(this.style.zIndex > curIndex) {
this.style.zIndex = this.style.zIndex - 1;
}
});
// Fire blur event for focused tooltip
qtiptisass.filter('.' + CLASS_FOCUS).qtiptisas('blur', event);
}
// Set the new z-index
tooltip.addClass(CLASS_FOCUS)[0].style.zIndex = newIndex;
}
}
return this;
};
PROTOTYPE.blur = function(event) {
if(!this.rendered || this.destroyed) { return this; }
// Set focused status to FALSE
this.tooltip.removeClass(CLASS_FOCUS);
// tooltipblur event
this._trigger('blur', [ this.tooltip.css('zIndex') ], event);
return this;
};
;PROTOTYPE.disable = function(state) {
if(this.destroyed) { return this; }
// If 'toggle' is passed, toggle the current state
if(state === 'toggle') {
state = !(this.rendered ? this.tooltip.hasClass(CLASS_DISABLED) : this.disabled);
}
// Disable if no state passed
else if('boolean' !== typeof state) {
state = TRUE;
}
if(this.rendered) {
this.tooltip.toggleClass(CLASS_DISABLED, state)
.attr('aria-disabled', state);
}
this.disabled = !!state;
return this;
};
PROTOTYPE.enable = function() { return this.disable(FALSE); };
;PROTOTYPE._createButton = function()
{
var self = this,
elements = this.elements,
tooltip = elements.tooltip,
button = this.options.content.button,
isString = typeof button === 'string',
close = isString ? button : 'Close tooltip';
if(elements.button) { elements.button.remove(); }
// Use custom button if one was supplied by user, else use default
if(button.jquery) {
elements.button = button;
}
else {
elements.button = $('<a />', {
'class': 'qtiptisas-close ' + (this.options.style.widget ? '' : NAMESPACE+'-icon'),
'title': close,
'aria-label': close
})
.prepend(
$('<span />', {
'class': 'ui-icon ui-icon-close',
'html': '&times;'
})
);
}
// Create button and setup attributes
elements.button.appendTo(elements.titlebar || tooltip)
.attr('role', 'button')
.click(function(event) {
if(!tooltip.hasClass(CLASS_DISABLED)) { self.hide(event); }
return FALSE;
});
};
PROTOTYPE._updateButton = function(button)
{
// Make sure tooltip is rendered and if not, return
if(!this.rendered) { return FALSE; }
var elem = this.elements.button;
if(button) { this._createButton(); }
else { elem.remove(); }
};
;// Widget class creator
function createWidgetClass(cls) {
return WIDGET.concat('').join(cls ? '-'+cls+' ' : ' ');
}
// Widget class setter method
PROTOTYPE._setWidget = function()
{
var on = this.options.style.widget,
elements = this.elements,
tooltip = elements.tooltip,
disabled = tooltip.hasClass(CLASS_DISABLED);
tooltip.removeClass(CLASS_DISABLED);
CLASS_DISABLED = on ? 'ui-state-disabled' : 'qtiptisas-disabled';
tooltip.toggleClass(CLASS_DISABLED, disabled);
tooltip.toggleClass('ui-helper-reset '+createWidgetClass(), on).toggleClass(CLASS_DEFAULT, this.options.style.def && !on);
if(elements.content) {
elements.content.toggleClass( createWidgetClass('content'), on);
}
if(elements.titlebar) {
elements.titlebar.toggleClass( createWidgetClass('header'), on);
}
if(elements.button) {
elements.button.toggleClass(NAMESPACE+'-icon', !on);
}
};
;function delay(callback, duration) {
// If tooltip has displayed, start hide timer
if(duration > 0) {
return setTimeout(
$.proxy(callback, this), duration
);
}
else{ callback.call(this); }
}
function showMethod(event) {
if(this.tooltip.hasClass(CLASS_DISABLED)) { return; }
// Clear hide timers
clearTimeout(this.timers.show);
clearTimeout(this.timers.hide);
// Start show timer
this.timers.show = delay.call(this,
function() { this.toggle(TRUE, event); },
this.options.show.delay
);
}
function hideMethod(event) {
if(this.tooltip.hasClass(CLASS_DISABLED) || this.destroyed) { return; }
// Check if new target was actually the tooltip element
var relatedTarget = $(event.relatedTarget),
ontoTooltip = relatedTarget.closest(SELECTOR)[0] === this.tooltip[0],
ontoTarget = relatedTarget[0] === this.options.show.target[0];
// Clear timers and stop animation queue
clearTimeout(this.timers.show);
clearTimeout(this.timers.hide);
// Prevent hiding if tooltip is fixed and event target is the tooltip.
// Or if mouse positioning is enabled and cursor momentarily overlaps
if(this !== relatedTarget[0] &&
(this.options.position.target === 'mouse' && ontoTooltip) ||
this.options.hide.fixed && (
(/mouse(out|leave|move)/).test(event.type) && (ontoTooltip || ontoTarget))
)
{
/* eslint-disable no-empty */
try {
event.preventDefault();
event.stopImmediatePropagation();
} catch(e) {}
/* eslint-enable no-empty */
return;
}
// If tooltip has displayed, start hide timer
this.timers.hide = delay.call(this,
function() { this.toggle(FALSE, event); },
this.options.hide.delay,
this
);
}
function inactiveMethod(event) {
if(this.tooltip.hasClass(CLASS_DISABLED) || !this.options.hide.inactive) { return; }
// Clear timer
clearTimeout(this.timers.inactive);
this.timers.inactive = delay.call(this,
function(){ this.hide(event); },
this.options.hide.inactive
);
}
function repositionMethod(event) {
if(this.rendered && this.tooltip[0].offsetWidth > 0) { this.reposition(event); }
}
// Store mouse coordinates
PROTOTYPE._storeMouse = function(event) {
(this.mouse = $.event.fix(event)).type = 'mousemove';
return this;
};
// Bind events
PROTOTYPE._bind = function(targets, events, method, suffix, context) {
if(!targets || !method || !events.length) { return; }
var ns = '.' + this._id + (suffix ? '-'+suffix : '');
$(targets).bind(
(events.split ? events : events.join(ns + ' ')) + ns,
$.proxy(method, context || this)
);
return this;
};
PROTOTYPE._unbind = function(targets, suffix) {
targets && $(targets).unbind('.' + this._id + (suffix ? '-'+suffix : ''));
return this;
};
// Global delegation helper
function delegate(selector, events, method) {
$(document.body).delegate(selector,
(events.split ? events : events.join('.'+NAMESPACE + ' ')) + '.'+NAMESPACE,
function() {
var api = QTIPTISAS.api[ $.attr(this, ATTR_ID) ];
api && !api.disabled && method.apply(api, arguments);
}
);
}
// Event trigger
PROTOTYPE._trigger = function(type, args, event) {
var callback = new $.Event('tooltip'+type);
callback.originalEvent = event && $.extend({}, event) || this.cache.event || NULL;
this.triggering = type;
this.tooltip.trigger(callback, [this].concat(args || []));
this.triggering = FALSE;
return !callback.isDefaultPrevented();
};
PROTOTYPE._bindEvents = function(showEvents, hideEvents, showTargets, hideTargets, showCallback, hideCallback) {
// Get tasrgets that lye within both
var similarTargets = showTargets.filter( hideTargets ).add( hideTargets.filter(showTargets) ),
toggleEvents = [];
// If hide and show targets are the same...
if(similarTargets.length) {
// Filter identical show/hide events
$.each(hideEvents, function(i, type) {
var showIndex = $.inArray(type, showEvents);
// Both events are identical, remove from both hide and show events
// and append to toggleEvents
showIndex > -1 && toggleEvents.push( showEvents.splice( showIndex, 1 )[0] );
});
// Toggle events are special case of identical show/hide events, which happen in sequence
if(toggleEvents.length) {
// Bind toggle events to the similar targets
this._bind(similarTargets, toggleEvents, function(event) {
var state = this.rendered ? this.tooltip[0].offsetWidth > 0 : false;
(state ? hideCallback : showCallback).call(this, event);
});
// Remove the similar targets from the regular show/hide bindings
showTargets = showTargets.not(similarTargets);
hideTargets = hideTargets.not(similarTargets);
}
}
// Apply show/hide/toggle events
this._bind(showTargets, showEvents, showCallback);
this._bind(hideTargets, hideEvents, hideCallback);
};
PROTOTYPE._assignInitialEvents = function(event) {
var options = this.options,
showTarget = options.show.target,
hideTarget = options.hide.target,
showEvents = options.show.event ? $.trim('' + options.show.event).split(' ') : [],
hideEvents = options.hide.event ? $.trim('' + options.hide.event).split(' ') : [];
// Catch remove/removeqtiptisas events on target element to destroy redundant tooltips
this._bind(this.elements.target, ['remove', 'removeqtiptisas'], function() {
this.destroy(true);
}, 'destroy');
/*
* Make sure hoverIntent functions properly by using mouseleave as a hide event if
* mouseenter/mouseout is used for show.event, even if it isn't in the users options.
*/
if(/mouse(over|enter)/i.test(options.show.event) && !/mouse(out|leave)/i.test(options.hide.event)) {
hideEvents.push('mouseleave');
}
/*
* Also make sure initial mouse targetting works correctly by caching mousemove coords
* on show targets before the tooltip has rendered. Also set onTarget when triggered to
* keep mouse tracking working.
*/
this._bind(showTarget, 'mousemove', function(moveEvent) {
this._storeMouse(moveEvent);
this.cache.onTarget = TRUE;
});
// Define hoverIntent function
function hoverIntent(hoverEvent) {
// Only continue if tooltip isn't disabled
if(this.disabled || this.destroyed) { return FALSE; }
// Cache the event data
this.cache.event = hoverEvent && $.event.fix(hoverEvent);
this.cache.target = hoverEvent && $(hoverEvent.target);
// Start the event sequence
clearTimeout(this.timers.show);
this.timers.show = delay.call(this,
function() { this.render(typeof hoverEvent === 'object' || options.show.ready); },
options.prerender ? 0 : options.show.delay
);
}
// Filter and bind events
this._bindEvents(showEvents, hideEvents, showTarget, hideTarget, hoverIntent, function() {
if(!this.timers) { return FALSE; }
clearTimeout(this.timers.show);
});
// Prerendering is enabled, create tooltip now
if(options.show.ready || options.prerender) { hoverIntent.call(this, event); }
};
// Event assignment method
PROTOTYPE._assignEvents = function() {
var self = this,
options = this.options,
posOptions = options.position,
tooltip = this.tooltip,
showTarget = options.show.target,
hideTarget = options.hide.target,
containerTarget = posOptions.container,
viewportTarget = posOptions.viewport,
documentTarget = $(document),
windowTarget = $(window),
showEvents = options.show.event ? $.trim('' + options.show.event).split(' ') : [],
hideEvents = options.hide.event ? $.trim('' + options.hide.event).split(' ') : [];
// Assign passed event callbacks
$.each(options.events, function(name, callback) {
self._bind(tooltip, name === 'toggle' ? ['tooltipshow','tooltiphide'] : ['tooltip'+name], callback, null, tooltip);
});
// Hide tooltips when leaving current window/frame (but not select/option elements)
if(/mouse(out|leave)/i.test(options.hide.event) && options.hide.leave === 'window') {
this._bind(documentTarget, ['mouseout', 'blur'], function(event) {
if(!/select|option/.test(event.target.nodeName) && !event.relatedTarget) {
this.hide(event);
}
});
}
// Enable hide.fixed by adding appropriate class
if(options.hide.fixed) {
hideTarget = hideTarget.add( tooltip.addClass(CLASS_FIXED) );
}
/*
* Make sure hoverIntent functions properly by using mouseleave to clear show timer if
* mouseenter/mouseout is used for show.event, even if it isn't in the users options.
*/
else if(/mouse(over|enter)/i.test(options.show.event)) {
this._bind(hideTarget, 'mouseleave', function() {
clearTimeout(this.timers.show);
});
}
// Hide tooltip on document mousedown if unfocus events are enabled
if(('' + options.hide.event).indexOf('unfocus') > -1) {
this._bind(containerTarget.closest('html'), ['mousedown', 'touchstart'], function(event) {
var elem = $(event.target),
enabled = this.rendered && !this.tooltip.hasClass(CLASS_DISABLED) && this.tooltip[0].offsetWidth > 0,
isAncestor = elem.parents(SELECTOR).filter(this.tooltip[0]).length > 0;
if(elem[0] !== this.target[0] && elem[0] !== this.tooltip[0] && !isAncestor &&
!this.target.has(elem[0]).length && enabled
) {
this.hide(event);
}
});
}
// Check if the tooltip hides when inactive
if('number' === typeof options.hide.inactive) {
// Bind inactive method to show target(s) as a custom event
this._bind(showTarget, 'qtiptisas-'+this.id+'-inactive', inactiveMethod, 'inactive');
// Define events which reset the 'inactive' event handler
this._bind(hideTarget.add(tooltip), QTIPTISAS.inactiveEvents, inactiveMethod);
}
// Filter and bind events
this._bindEvents(showEvents, hideEvents, showTarget, hideTarget, showMethod, hideMethod);
// Mouse movement bindings
this._bind(showTarget.add(tooltip), 'mousemove', function(event) {
// Check if the tooltip hides when mouse is moved a certain distance
if('number' === typeof options.hide.distance) {
var origin = this.cache.origin || {},
limit = this.options.hide.distance,
abs = Math.abs;
// Check if the movement has gone beyond the limit, and hide it if so
if(abs(event.pageX - origin.pageX) >= limit || abs(event.pageY - origin.pageY) >= limit) {
this.hide(event);
}
}
// Cache mousemove coords on show targets
this._storeMouse(event);
});
// Mouse positioning events
if(posOptions.target === 'mouse') {
// If mouse adjustment is on...
if(posOptions.adjust.mouse) {
// Apply a mouseleave event so we don't get problems with overlapping
if(options.hide.event) {
// Track if we're on the target or not
this._bind(showTarget, ['mouseenter', 'mouseleave'], function(event) {
if(!this.cache) {return FALSE; }
this.cache.onTarget = event.type === 'mouseenter';
});
}
// Update tooltip position on mousemove
this._bind(documentTarget, 'mousemove', function(event) {
// Update the tooltip position only if the tooltip is visible and adjustment is enabled
if(this.rendered && this.cache.onTarget && !this.tooltip.hasClass(CLASS_DISABLED) && this.tooltip[0].offsetWidth > 0) {
this.reposition(event);
}
});
}
}
// Adjust positions of the tooltip on window resize if enabled
if(posOptions.adjust.resize || viewportTarget.length) {
this._bind( $.event.special.resize ? viewportTarget : windowTarget, 'resize', repositionMethod );
}
// Adjust tooltip position on scroll of the window or viewport element if present
if(posOptions.adjust.scroll) {
this._bind( windowTarget.add(posOptions.container), 'scroll', repositionMethod );
}
};
// Un-assignment method
PROTOTYPE._unassignEvents = function() {
var options = this.options,
showTargets = options.show.target,
hideTargets = options.hide.target,
targets = $.grep([
this.elements.target[0],
this.rendered && this.tooltip[0],
options.position.container[0],
options.position.viewport[0],
options.position.container.closest('html')[0], // unfocus
window,
document
], function(i) {
return typeof i === 'object';
});
// Add show and hide targets if they're valid
if(showTargets && showTargets.toArray) {
targets = targets.concat(showTargets.toArray());
}
if(hideTargets && hideTargets.toArray) {
targets = targets.concat(hideTargets.toArray());
}
// Unbind the events
this._unbind(targets)
._unbind(targets, 'destroy')
._unbind(targets, 'inactive');
};
// Apply common event handlers using delegate (avoids excessive .bind calls!)
$(function() {
delegate(SELECTOR, ['mouseenter', 'mouseleave'], function(event) {
var state = event.type === 'mouseenter',
tooltip = $(event.currentTarget),
target = $(event.relatedTarget || event.target),
options = this.options;
// On mouseenter...
if(state) {
// Focus the tooltip on mouseenter (z-index stacking)
this.focus(event);
// Clear hide timer on tooltip hover to prevent it from closing
tooltip.hasClass(CLASS_FIXED) && !tooltip.hasClass(CLASS_DISABLED) && clearTimeout(this.timers.hide);
}
// On mouseleave...
else {
// When mouse tracking is enabled, hide when we leave the tooltip and not onto the show target (if a hide event is set)
if(options.position.target === 'mouse' && options.position.adjust.mouse &&
options.hide.event && options.show.target && !target.closest(options.show.target[0]).length) {
this.hide(event);
}
}
// Add hover class
tooltip.toggleClass(CLASS_HOVER, state);
});
// Define events which reset the 'inactive' event handler
delegate('['+ATTR_ID+']', INACTIVE_EVENTS, inactiveMethod);
});
;// Initialization method
function init(elem, id, opts) {
var obj, posOptions, attr, config, title,
// Setup element references
docBody = $(document.body),
// Use document body instead of document element if needed
newTarget = elem[0] === document ? docBody : elem,
// Grab metadata from element if plugin is present
metadata = elem.metadata ? elem.metadata(opts.metadata) : NULL,
// If metadata type if HTML5, grab 'name' from the object instead, or use the regular data object otherwise
metadata5 = opts.metadata.type === 'html5' && metadata ? metadata[opts.metadata.name] : NULL,
// Grab data from metadata.name (or data-qtiptisasopts as fallback) using .data() method,
html5 = elem.data(opts.metadata.name || 'qtiptisasopts');
// If we don't get an object returned attempt to parse it manualyl without parseJSON
/* eslint-disable no-empty */
try { html5 = typeof html5 === 'string' ? $.parseJSON(html5) : html5; }
catch(e) {}
/* eslint-enable no-empty */
// Merge in and sanitize metadata
config = $.extend(TRUE, {}, QTIPTISAS.defaults, opts,
typeof html5 === 'object' ? sanitizeOptions(html5) : NULL,
sanitizeOptions(metadata5 || metadata));
// Re-grab our positioning options now we've merged our metadata and set id to passed value
posOptions = config.position;
config.id = id;
// Setup missing content if none is detected
if('boolean' === typeof config.content.text) {
attr = elem.attr(config.content.attr);
// Grab from supplied attribute if available
if(config.content.attr !== FALSE && attr) { config.content.text = attr; }
// No valid content was found, abort render
else { return FALSE; }
}
// Setup target options
if(!posOptions.container.length) { posOptions.container = docBody; }
if(posOptions.target === FALSE) { posOptions.target = newTarget; }
if(config.show.target === FALSE) { config.show.target = newTarget; }
if(config.show.solo === TRUE) { config.show.solo = posOptions.container.closest('body'); }
if(config.hide.target === FALSE) { config.hide.target = newTarget; }
if(config.position.viewport === TRUE) { config.position.viewport = posOptions.container; }
// Ensure we only use a single container
posOptions.container = posOptions.container.eq(0);
// Convert position corner values into x and y strings
posOptions.at = new CORNER(posOptions.at, TRUE);
posOptions.my = new CORNER(posOptions.my);
// Destroy previous tooltip if overwrite is enabled, or skip element if not
if(elem.data(NAMESPACE)) {
if(config.overwrite) {
elem.qtiptisas('destroy', true);
}
else if(config.overwrite === FALSE) {
return FALSE;
}
}
// Add has-qtiptisas attribute
elem.attr(ATTR_HAS, id);
// Remove title attribute and store it if present
if(config.suppress && (title = elem.attr('title'))) {
// Final attr call fixes event delegatiom and IE default tooltip showing problem
elem.removeAttr('title').attr(oldtitle, title).attr('title', '');
}
// Initialize the tooltip and add API reference
obj = new QTipTisas(elem, config, id, !!attr);
elem.data(NAMESPACE, obj);
return obj;
}
// jQuery $.fn extension method
QTIPTISAS = $.fn.qtiptisas = function(options, notation, newValue)
{
var command = ('' + options).toLowerCase(), // Parse command
returned = NULL,
args = $.makeArray(arguments).slice(1),
event = args[args.length - 1],
opts = this[0] ? $.data(this[0], NAMESPACE) : NULL;
// Check for API request
if(!arguments.length && opts || command === 'api') {
return opts;
}
// Execute API command if present
else if('string' === typeof options) {
this.each(function() {
var api = $.data(this, NAMESPACE);
if(!api) { return TRUE; }
// Cache the event if possible
if(event && event.timeStamp) { api.cache.event = event; }
// Check for specific API commands
if(notation && (command === 'option' || command === 'options')) {
if(newValue !== undefined || $.isPlainObject(notation)) {
api.set(notation, newValue);
}
else {
returned = api.get(notation);
return FALSE;
}
}
// Execute API command
else if(api[command]) {
api[command].apply(api, args);
}
});
return returned !== NULL ? returned : this;
}
// No API commands. validate provided options and setup qTipTisass
else if('object' === typeof options || !arguments.length) {
// Sanitize options first
opts = sanitizeOptions($.extend(TRUE, {}, options));
return this.each(function(i) {
var api, id;
// Find next available ID, or use custom ID if provided
id = $.isArray(opts.id) ? opts.id[i] : opts.id;
id = !id || id === FALSE || id.length < 1 || QTIPTISAS.api[id] ? QTIPTISAS.nextid++ : id;
// Initialize the qTipTisas and re-grab newly sanitized options
api = init($(this), id, opts);
if(api === FALSE) { return TRUE; }
else { QTIPTISAS.api[id] = api; }
// Initialize plugins
$.each(PLUGINS, function() {
if(this.initialize === 'initialize') { this(api); }
});
// Assign initial pre-render events
api._assignInitialEvents(event);
});
}
};
// Expose class
$.qtiptisas = QTipTisas;
// Populated in render method
QTIPTISAS.api = {};
;$.each({
/* Allow other plugins to successfully retrieve the title of an element with a qTipTisas applied */
attr: function(attr, val) {
if(this.length) {
var self = this[0],
title = 'title',
api = $.data(self, 'qtiptisas');
if(attr === title && api && api.options && 'object' === typeof api && 'object' === typeof api.options && api.options.suppress) {
if(arguments.length < 2) {
return $.attr(self, oldtitle);
}
// If qTipTisas is rendered and title was originally used as content, update it
if(api && api.options.content.attr === title && api.cache.attr) {
api.set('content.text', val);
}
// Use the regular attr method to set, then cache the result
return this.attr(oldtitle, val);
}
}
return $.fn['attr'+replaceSuffix].apply(this, arguments);
},
/* Allow clone to correctly retrieve cached title attributes */
clone: function(keepData) {
// Clone our element using the real clone method
var elems = $.fn['clone'+replaceSuffix].apply(this, arguments);
// Grab all elements with an oldtitle set, and change it to regular title attribute, if keepData is false
if(!keepData) {
elems.filter('['+oldtitle+']').attr('title', function() {
return $.attr(this, oldtitle);
})
.removeAttr(oldtitle);
}
return elems;
}
}, function(name, func) {
if(!func || $.fn[name+replaceSuffix]) { return TRUE; }
var old = $.fn[name+replaceSuffix] = $.fn[name];
$.fn[name] = function() {
return func.apply(this, arguments) || old.apply(this, arguments);
};
});
/* Fire off 'removeqtiptisas' handler in $.cleanData if jQuery UI not present (it already does similar).
* This snippet is taken directly from jQuery UI source code found here:
* http://code.jquery.com/ui/jquery-ui-git.js
*/
if(!$.ui) {
$['cleanData'+replaceSuffix] = $.cleanData;
$.cleanData = function( elems ) {
for(var i = 0, elem; (elem = $( elems[i] )).length; i++) {
if(elem.attr(ATTR_HAS)) {
/* eslint-disable no-empty */
try { elem.triggerHandler('removeqtiptisas'); }
catch( e ) {}
/* eslint-enable no-empty */
}
}
$['cleanData'+replaceSuffix].apply(this, arguments);
};
}
;// qTipTisas version
QTIPTISAS.version = '3.0.3';
// Base ID for all qTipTisass
QTIPTISAS.nextid = 0;
// Inactive events array
QTIPTISAS.inactiveEvents = INACTIVE_EVENTS;
// Base z-index for all qTipTisass
QTIPTISAS.zindex = 15000;
// Define configuration defaults
QTIPTISAS.defaults = {
prerender: FALSE,
id: FALSE,
overwrite: TRUE,
suppress: TRUE,
content: {
text: TRUE,
attr: 'title',
title: FALSE,
button: FALSE
},
position: {
my: 'top left',
at: 'bottom right',
target: FALSE,
container: FALSE,
viewport: FALSE,
adjust: {
x: 0, y: 0,
mouse: TRUE,
scroll: TRUE,
resize: TRUE,
method: 'flipinvert flipinvert'
},
effect: function(api, pos) {
$(this).animate(pos, {
duration: 200,
queue: FALSE
});
}
},
show: {
target: FALSE,
event: 'mouseenter',
effect: TRUE,
delay: 90,
solo: FALSE,
ready: FALSE,
autofocus: FALSE
},
hide: {
target: FALSE,
event: 'mouseleave',
effect: TRUE,
delay: 0,
fixed: FALSE,
inactive: FALSE,
leave: 'window',
distance: FALSE
},
style: {
classes: '',
widget: FALSE,
width: FALSE,
height: FALSE,
def: TRUE
},
events: {
render: NULL,
move: NULL,
show: NULL,
hide: NULL,
toggle: NULL,
visible: NULL,
hidden: NULL,
focus: NULL,
blur: NULL
}
};
;var TIP,
createVML,
SCALE,
PIXEL_RATIO,
BACKING_STORE_RATIO,
// Common CSS strings
MARGIN = 'margin',
BORDER = 'border',
COLOR = 'color',
BG_COLOR = 'background-color',
TRANSPARENT = 'transparent',
IMPORTANT = ' !important',
// Check if the browser supports <canvas/> elements
HASCANVAS = !!document.createElement('canvas').getContext,
// Invalid colour values used in parseColours()
INVALID = /rgba?\(0, 0, 0(, 0)?\)|transparent|#123456/i;
// Camel-case method, taken from jQuery source
// http://code.jquery.com/jquery-1.8.0.js
function camel(s) { return s.charAt(0).toUpperCase() + s.slice(1); }
/*
* Modified from Modernizr's testPropsAll()
* http://modernizr.com/downloads/modernizr-latest.js
*/
var cssProps = {}, cssPrefixes = ['Webkit', 'O', 'Moz', 'ms'];
function vendorCss(elem, prop) {
var ucProp = prop.charAt(0).toUpperCase() + prop.slice(1),
props = (prop + ' ' + cssPrefixes.join(ucProp + ' ') + ucProp).split(' '),
cur, val, i = 0;
// If the property has already been mapped...
if(cssProps[prop]) { return elem.css(cssProps[prop]); }
while(cur = props[i++]) {
if((val = elem.css(cur)) !== undefined) {
cssProps[prop] = cur;
return val;
}
}
}
// Parse a given elements CSS property into an int
function intCss(elem, prop) {
return Math.ceil(parseFloat(vendorCss(elem, prop)));
}
// VML creation (for IE only)
if(!HASCANVAS) {
createVML = function(tag, props, style) {
return '<qtiptisasvml:'+tag+' xmlns="urn:schemas-microsoft.com:vml" class="qtiptisas-vml" '+(props||'')+
' style="behavior: url(#default#VML); '+(style||'')+ '" />';
};
}
// Canvas only definitions
else {
PIXEL_RATIO = window.devicePixelRatio || 1;
BACKING_STORE_RATIO = (function() {
var context = document.createElement('canvas').getContext('2d');
return context.backingStorePixelRatio || context.webkitBackingStorePixelRatio || context.mozBackingStorePixelRatio ||
context.msBackingStorePixelRatio || context.oBackingStorePixelRatio || 1;
})();
SCALE = PIXEL_RATIO / BACKING_STORE_RATIO;
}
function Tip(qtiptisas, options) {
this._ns = 'tip';
this.options = options;
this.offset = options.offset;
this.size = [ options.width, options.height ];
// Initialize
this.qtiptisas = qtiptisas;
this.init(qtiptisas);
}
$.extend(Tip.prototype, {
init: function(qtiptisas) {
var context, tip;
// Create tip element and prepend to the tooltip
tip = this.element = qtiptisas.elements.tip = $('<div />', { 'class': NAMESPACE+'-tip' }).prependTo(qtiptisas.tooltip);
// Create tip drawing element(s)
if(HASCANVAS) {
// save() as soon as we create the canvas element so FF2 doesn't bork on our first restore()!
context = $('<canvas />').appendTo(this.element)[0].getContext('2d');
// Setup constant parameters
context.lineJoin = 'miter';
context.miterLimit = 100000;
context.save();
}
else {
context = createVML('shape', 'coordorigin="0,0"', 'position:absolute;');
this.element.html(context + context);
// Prevent mousing down on the tip since it causes problems with .live() handling in IE due to VML
qtiptisas._bind( $('*', tip).add(tip), ['click', 'mousedown'], function(event) { event.stopPropagation(); }, this._ns);
}
// Bind update events
qtiptisas._bind(qtiptisas.tooltip, 'tooltipmove', this.reposition, this._ns, this);
// Create it
this.create();
},
_swapDimensions: function() {
this.size[0] = this.options.height;
this.size[1] = this.options.width;
},
_resetDimensions: function() {
this.size[0] = this.options.width;
this.size[1] = this.options.height;
},
_useTitle: function(corner) {
var titlebar = this.qtiptisas.elements.titlebar;
return titlebar && (
corner.y === TOP || corner.y === CENTER && this.element.position().top + this.size[1] / 2 + this.options.offset < titlebar.outerHeight(TRUE)
);
},
_parseCorner: function(corner) {
var my = this.qtiptisas.options.position.my;
// Detect corner and mimic properties
if(corner === FALSE || my === FALSE) {
corner = FALSE;
}
else if(corner === TRUE) {
corner = new CORNER( my.string() );
}
else if(!corner.string) {
corner = new CORNER(corner);
corner.fixed = TRUE;
}
return corner;
},
_parseWidth: function(corner, side, use) {
var elements = this.qtiptisas.elements,
prop = BORDER + camel(side) + 'Width';
return (use ? intCss(use, prop) :
intCss(elements.content, prop) ||
intCss(this._useTitle(corner) && elements.titlebar || elements.content, prop) ||
intCss(elements.tooltip, prop)
) || 0;
},
_parseRadius: function(corner) {
var elements = this.qtiptisas.elements,
prop = BORDER + camel(corner.y) + camel(corner.x) + 'Radius';
return BROWSER.ie < 9 ? 0 :
intCss(this._useTitle(corner) && elements.titlebar || elements.content, prop) ||
intCss(elements.tooltip, prop) || 0;
},
_invalidColour: function(elem, prop, compare) {
var val = elem.css(prop);
return !val || compare && val === elem.css(compare) || INVALID.test(val) ? FALSE : val;
},
_parseColours: function(corner) {
var elements = this.qtiptisas.elements,
tip = this.element.css('cssText', ''),
borderSide = BORDER + camel(corner[ corner.precedance ]) + camel(COLOR),
colorElem = this._useTitle(corner) && elements.titlebar || elements.content,
css = this._invalidColour, color = [];
// Attempt to detect the background colour from various elements, left-to-right precedance
color[0] = css(tip, BG_COLOR) || css(colorElem, BG_COLOR) || css(elements.content, BG_COLOR) ||
css(elements.tooltip, BG_COLOR) || tip.css(BG_COLOR);
// Attempt to detect the correct border side colour from various elements, left-to-right precedance
color[1] = css(tip, borderSide, COLOR) || css(colorElem, borderSide, COLOR) ||
css(elements.content, borderSide, COLOR) || css(elements.tooltip, borderSide, COLOR) || elements.tooltip.css(borderSide);
// Reset background and border colours
$('*', tip).add(tip).css('cssText', BG_COLOR+':'+TRANSPARENT+IMPORTANT+';'+BORDER+':0'+IMPORTANT+';');
return color;
},
_calculateSize: function(corner) {
var y = corner.precedance === Y,
width = this.options.width,
height = this.options.height,
isCenter = corner.abbrev() === 'c',
base = (y ? width: height) * (isCenter ? 0.5 : 1),
pow = Math.pow,
round = Math.round,
bigHyp, ratio, result,
smallHyp = Math.sqrt( pow(base, 2) + pow(height, 2) ),
hyp = [
this.border / base * smallHyp,
this.border / height * smallHyp
];
hyp[2] = Math.sqrt( pow(hyp[0], 2) - pow(this.border, 2) );
hyp[3] = Math.sqrt( pow(hyp[1], 2) - pow(this.border, 2) );
bigHyp = smallHyp + hyp[2] + hyp[3] + (isCenter ? 0 : hyp[0]);
ratio = bigHyp / smallHyp;
result = [ round(ratio * width), round(ratio * height) ];
return y ? result : result.reverse();
},
// Tip coordinates calculator
_calculateTip: function(corner, size, scale) {
scale = scale || 1;
size = size || this.size;
var width = size[0] * scale,
height = size[1] * scale,
width2 = Math.ceil(width / 2), height2 = Math.ceil(height / 2),
// Define tip coordinates in terms of height and width values
tips = {
br: [0,0, width,height, width,0],
bl: [0,0, width,0, 0,height],
tr: [0,height, width,0, width,height],
tl: [0,0, 0,height, width,height],
tc: [0,height, width2,0, width,height],
bc: [0,0, width,0, width2,height],
rc: [0,0, width,height2, 0,height],
lc: [width,0, width,height, 0,height2]
};
// Set common side shapes
tips.lt = tips.br; tips.rt = tips.bl;
tips.lb = tips.tr; tips.rb = tips.tl;
return tips[ corner.abbrev() ];
},
// Tip coordinates drawer (canvas)
_drawCoords: function(context, coords) {
context.beginPath();
context.moveTo(coords[0], coords[1]);
context.lineTo(coords[2], coords[3]);
context.lineTo(coords[4], coords[5]);
context.closePath();
},
create: function() {
// Determine tip corner
var c = this.corner = (HASCANVAS || BROWSER.ie) && this._parseCorner(this.options.corner);
// If we have a tip corner...
this.enabled = !!this.corner && this.corner.abbrev() !== 'c';
if(this.enabled) {
// Cache it
this.qtiptisas.cache.corner = c.clone();
// Create it
this.update();
}
// Toggle tip element
this.element.toggle(this.enabled);
return this.corner;
},
update: function(corner, position) {
if(!this.enabled) { return this; }
var elements = this.qtiptisas.elements,
tip = this.element,
inner = tip.children(),
options = this.options,
curSize = this.size,
mimic = options.mimic,
round = Math.round,
color, precedance, context,
coords, bigCoords, translate, newSize, border;
// Re-determine tip if not already set
if(!corner) { corner = this.qtiptisas.cache.corner || this.corner; }
// Use corner property if we detect an invalid mimic value
if(mimic === FALSE) { mimic = corner; }
// Otherwise inherit mimic properties from the corner object as necessary
else {
mimic = new CORNER(mimic);
mimic.precedance = corner.precedance;
if(mimic.x === 'inherit') { mimic.x = corner.x; }
else if(mimic.y === 'inherit') { mimic.y = corner.y; }
else if(mimic.x === mimic.y) {
mimic[ corner.precedance ] = corner[ corner.precedance ];
}
}
precedance = mimic.precedance;
// Ensure the tip width.height are relative to the tip position
if(corner.precedance === X) { this._swapDimensions(); }
else { this._resetDimensions(); }
// Update our colours
color = this.color = this._parseColours(corner);
// Detect border width, taking into account colours
if(color[1] !== TRANSPARENT) {
// Grab border width
border = this.border = this._parseWidth(corner, corner[corner.precedance]);
// If border width isn't zero, use border color as fill if it's not invalid (1.0 style tips)
if(options.border && border < 1 && !INVALID.test(color[1])) { color[0] = color[1]; }
// Set border width (use detected border width if options.border is true)
this.border = border = options.border !== TRUE ? options.border : border;
}
// Border colour was invalid, set border to zero
else { this.border = border = 0; }
// Determine tip size
newSize = this.size = this._calculateSize(corner);
tip.css({
width: newSize[0],
height: newSize[1],
lineHeight: newSize[1]+'px'
});
// Calculate tip translation
if(corner.precedance === Y) {
translate = [
round(mimic.x === LEFT ? border : mimic.x === RIGHT ? newSize[0] - curSize[0] - border : (newSize[0] - curSize[0]) / 2),
round(mimic.y === TOP ? newSize[1] - curSize[1] : 0)
];
}
else {
translate = [
round(mimic.x === LEFT ? newSize[0] - curSize[0] : 0),
round(mimic.y === TOP ? border : mimic.y === BOTTOM ? newSize[1] - curSize[1] - border : (newSize[1] - curSize[1]) / 2)
];
}
// Canvas drawing implementation
if(HASCANVAS) {
// Grab canvas context and clear/save it
context = inner[0].getContext('2d');
context.restore(); context.save();
context.clearRect(0,0,6000,6000);
// Calculate coordinates
coords = this._calculateTip(mimic, curSize, SCALE);
bigCoords = this._calculateTip(mimic, this.size, SCALE);
// Set the canvas size using calculated size
inner.attr(WIDTH, newSize[0] * SCALE).attr(HEIGHT, newSize[1] * SCALE);
inner.css(WIDTH, newSize[0]).css(HEIGHT, newSize[1]);
// Draw the outer-stroke tip
this._drawCoords(context, bigCoords);
context.fillStyle = color[1];
context.fill();
// Draw the actual tip
context.translate(translate[0] * SCALE, translate[1] * SCALE);
this._drawCoords(context, coords);
context.fillStyle = color[0];
context.fill();
}
// VML (IE Proprietary implementation)
else {
// Calculate coordinates
coords = this._calculateTip(mimic);
// Setup coordinates string
coords = 'm' + coords[0] + ',' + coords[1] + ' l' + coords[2] +
',' + coords[3] + ' ' + coords[4] + ',' + coords[5] + ' xe';
// Setup VML-specific offset for pixel-perfection
translate[2] = border && /^(r|b)/i.test(corner.string()) ?
BROWSER.ie === 8 ? 2 : 1 : 0;
// Set initial CSS
inner.css({
coordsize: newSize[0]+border + ' ' + newSize[1]+border,
antialias: ''+(mimic.string().indexOf(CENTER) > -1),
left: translate[0] - translate[2] * Number(precedance === X),
top: translate[1] - translate[2] * Number(precedance === Y),
width: newSize[0] + border,
height: newSize[1] + border
})
.each(function(i) {
var $this = $(this);
// Set shape specific attributes
$this[ $this.prop ? 'prop' : 'attr' ]({
coordsize: newSize[0]+border + ' ' + newSize[1]+border,
path: coords,
fillcolor: color[0],
filled: !!i,
stroked: !i
})
.toggle(!!(border || i));
// Check if border is enabled and add stroke element
!i && $this.html( createVML(
'stroke', 'weight="'+border*2+'px" color="'+color[1]+'" miterlimit="1000" joinstyle="miter"'
) );
});
}
// Opera bug #357 - Incorrect tip position
// https://github.com/Craga89/qTip/issues/367
window.opera && setTimeout(function() {
elements.tip.css({
display: 'inline-block',
visibility: 'visible'
});
}, 1);
// Position if needed
if(position !== FALSE) { this.calculate(corner, newSize); }
},
calculate: function(corner, size) {
if(!this.enabled) { return FALSE; }
var self = this,
elements = this.qtiptisas.elements,
tip = this.element,
userOffset = this.options.offset,
position = {},
precedance, corners;
// Inherit corner if not provided
corner = corner || this.corner;
precedance = corner.precedance;
// Determine which tip dimension to use for adjustment
size = size || this._calculateSize(corner);
// Setup corners and offset array
corners = [ corner.x, corner.y ];
if(precedance === X) { corners.reverse(); }
// Calculate tip position
$.each(corners, function(i, side) {
var b, bc, br;
if(side === CENTER) {
b = precedance === Y ? LEFT : TOP;
position[ b ] = '50%';
position[MARGIN+'-' + b] = -Math.round(size[ precedance === Y ? 0 : 1 ] / 2) + userOffset;
}
else {
b = self._parseWidth(corner, side, elements.tooltip);
bc = self._parseWidth(corner, side, elements.content);
br = self._parseRadius(corner);
position[ side ] = Math.max(-self.border, i ? bc : userOffset + (br > b ? br : -b));
}
});
// Adjust for tip size
position[ corner[precedance] ] -= size[ precedance === X ? 0 : 1 ];
// Set and return new position
tip.css({ margin: '', top: '', bottom: '', left: '', right: '' }).css(position);
return position;
},
reposition: function(event, api, pos) {
if(!this.enabled) { return; }
var cache = api.cache,
newCorner = this.corner.clone(),
adjust = pos.adjusted,
method = api.options.position.adjust.method.split(' '),
horizontal = method[0],
vertical = method[1] || method[0],
shift = { left: FALSE, top: FALSE, x: 0, y: 0 },
offset, css = {}, props;
function shiftflip(direction, precedance, popposite, side, opposite) {
// Horizontal - Shift or flip method
if(direction === SHIFT && newCorner.precedance === precedance && adjust[side] && newCorner[popposite] !== CENTER) {
newCorner.precedance = newCorner.precedance === X ? Y : X;
}
else if(direction !== SHIFT && adjust[side]){
newCorner[precedance] = newCorner[precedance] === CENTER ?
adjust[side] > 0 ? side : opposite :
newCorner[precedance] === side ? opposite : side;
}
}
function shiftonly(xy, side, opposite) {
if(newCorner[xy] === CENTER) {
css[MARGIN+'-'+side] = shift[xy] = offset[MARGIN+'-'+side] - adjust[side];
}
else {
props = offset[opposite] !== undefined ?
[ adjust[side], -offset[side] ] : [ -adjust[side], offset[side] ];
if( (shift[xy] = Math.max(props[0], props[1])) > props[0] ) {
pos[side] -= adjust[side];
shift[side] = FALSE;
}
css[ offset[opposite] !== undefined ? opposite : side ] = shift[xy];
}
}
// If our tip position isn't fixed e.g. doesn't adjust with viewport...
if(this.corner.fixed !== TRUE) {
// Perform shift/flip adjustments
shiftflip(horizontal, X, Y, LEFT, RIGHT);
shiftflip(vertical, Y, X, TOP, BOTTOM);
// Update and redraw the tip if needed (check cached details of last drawn tip)
if(newCorner.string() !== cache.corner.string() || cache.cornerTop !== adjust.top || cache.cornerLeft !== adjust.left) {
this.update(newCorner, FALSE);
}
}
// Setup tip offset properties
offset = this.calculate(newCorner);
// Readjust offset object to make it left/top
if(offset.right !== undefined) { offset.left = -offset.right; }
if(offset.bottom !== undefined) { offset.top = -offset.bottom; }
offset.user = this.offset;
// Perform shift adjustments
shift.left = horizontal === SHIFT && !!adjust.left;
if(shift.left) {
shiftonly(X, LEFT, RIGHT);
}
shift.top = vertical === SHIFT && !!adjust.top;
if(shift.top) {
shiftonly(Y, TOP, BOTTOM);
}
/*
* If the tip is adjusted in both dimensions, or in a
* direction that would cause it to be anywhere but the
* outer border, hide it!
*/
this.element.css(css).toggle(
!(shift.x && shift.y || newCorner.x === CENTER && shift.y || newCorner.y === CENTER && shift.x)
);
// Adjust position to accomodate tip dimensions
pos.left -= offset.left.charAt ? offset.user :
horizontal !== SHIFT || shift.top || !shift.left && !shift.top ? offset.left + this.border : 0;
pos.top -= offset.top.charAt ? offset.user :
vertical !== SHIFT || shift.left || !shift.left && !shift.top ? offset.top + this.border : 0;
// Cache details
cache.cornerLeft = adjust.left; cache.cornerTop = adjust.top;
cache.corner = newCorner.clone();
},
destroy: function() {
// Unbind events
this.qtiptisas._unbind(this.qtiptisas.tooltip, this._ns);
// Remove the tip element(s)
if(this.qtiptisas.elements.tip) {
this.qtiptisas.elements.tip.find('*')
.remove().end().remove();
}
}
});
TIP = PLUGINS.tip = function(api) {
return new Tip(api, api.options.style.tip);
};
// Initialize tip on render
TIP.initialize = 'render';
// Setup plugin sanitization options
TIP.sanitize = function(options) {
if(options.style && 'tip' in options.style) {
var opts = options.style.tip;
if(typeof opts !== 'object') { opts = options.style.tip = { corner: opts }; }
if(!(/string|boolean/i).test(typeof opts.corner)) { opts.corner = TRUE; }
}
};
// Add new option checks for the plugin
CHECKS.tip = {
'^position.my|style.tip.(corner|mimic|border)$': function() {
// Make sure a tip can be drawn
this.create();
// Reposition the tooltip
this.qtiptisas.reposition();
},
'^style.tip.(height|width)$': function(obj) {
// Re-set dimensions and redraw the tip
this.size = [ obj.width, obj.height ];
this.update();
// Reposition the tooltip
this.qtiptisas.reposition();
},
'^content.title|style.(classes|widget)$': function() {
this.update();
}
};
// Extend original qTipTisas defaults
$.extend(TRUE, QTIPTISAS.defaults, {
style: {
tip: {
corner: TRUE,
mimic: FALSE,
width: 6,
height: 6,
border: TRUE,
offset: 0
}
}
});
;var MODAL, OVERLAY,
MODALCLASS = 'qtiptisas-modal',
MODALSELECTOR = '.'+MODALCLASS;
OVERLAY = function()
{
var self = this,
focusableElems = {},
current,
prevState,
elem;
// Modified code from jQuery UI 1.10.0 source
// http://code.jquery.com/ui/1.10.0/jquery-ui.js
function focusable(element) {
// Use the defined focusable checker when possible
if($.expr[':'].focusable) { return $.expr[':'].focusable; }
var isTabIndexNotNaN = !isNaN($.attr(element, 'tabindex')),
nodeName = element.nodeName && element.nodeName.toLowerCase(),
map, mapName, img;
if('area' === nodeName) {
map = element.parentNode;
mapName = map.name;
if(!element.href || !mapName || map.nodeName.toLowerCase() !== 'map') {
return false;
}
img = $('img[usemap=#' + mapName + ']')[0];
return !!img && img.is(':visible');
}
return /input|select|textarea|button|object/.test( nodeName ) ?
!element.disabled :
'a' === nodeName ?
element.href || isTabIndexNotNaN :
isTabIndexNotNaN
;
}
// Focus inputs using cached focusable elements (see update())
function focusInputs(blurElems) {
// Blurring body element in IE causes window.open windows to unfocus!
if(focusableElems.length < 1 && blurElems.length) { blurElems.not('body').blur(); }
// Focus the inputs
else { focusableElems.first().focus(); }
}
// Steal focus from elements outside tooltip
function stealFocus(event) {
if(!elem.is(':visible')) { return; }
var target = $(event.target),
tooltip = current.tooltip,
container = target.closest(SELECTOR),
targetOnTop;
// Determine if input container target is above this
targetOnTop = container.length < 1 ? FALSE :
parseInt(container[0].style.zIndex, 10) > parseInt(tooltip[0].style.zIndex, 10);
// If we're showing a modal, but focus has landed on an input below
// this modal, divert focus to the first visible input in this modal
// or if we can't find one... the tooltip itself
if(!targetOnTop && target.closest(SELECTOR)[0] !== tooltip[0]) {
focusInputs(target);
}
}
$.extend(self, {
init: function() {
// Create document overlay
elem = self.elem = $('<div />', {
id: 'qtiptisas-overlay',
html: '<div></div>',
mousedown: function() { return FALSE; }
})
.hide();
// Make sure we can't focus anything outside the tooltip
$(document.body).bind('focusin'+MODALSELECTOR, stealFocus);
// Apply keyboard "Escape key" close handler
$(document).bind('keydown'+MODALSELECTOR, function(event) {
if(current && current.options.show.modal.escape && event.keyCode === 27) {
current.hide(event);
}
});
// Apply click handler for blur option
elem.bind('click'+MODALSELECTOR, function(event) {
if(current && current.options.show.modal.blur) {
current.hide(event);
}
});
return self;
},
update: function(api) {
// Update current API reference
current = api;
// Update focusable elements if enabled
if(api.options.show.modal.stealfocus !== FALSE) {
focusableElems = api.tooltip.find('*').filter(function() {
return focusable(this);
});
}
else { focusableElems = []; }
},
toggle: function(api, state, duration) {
var tooltip = api.tooltip,
options = api.options.show.modal,
effect = options.effect,
type = state ? 'show': 'hide',
visible = elem.is(':visible'),
visibleModals = $(MODALSELECTOR).filter(':visible:not(:animated)').not(tooltip);
// Set active tooltip API reference
self.update(api);
// If the modal can steal the focus...
// Blur the current item and focus anything in the modal we an
if(state && options.stealfocus !== FALSE) {
focusInputs( $(':focus') );
}
// Toggle backdrop cursor style on show
elem.toggleClass('blurs', options.blur);
// Append to body on show
if(state) {
elem.appendTo(document.body);
}
// Prevent modal from conflicting with show.solo, and don't hide backdrop is other modals are visible
if(elem.is(':animated') && visible === state && prevState !== FALSE || !state && visibleModals.length) {
return self;
}
// Stop all animations
elem.stop(TRUE, FALSE);
// Use custom function if provided
if($.isFunction(effect)) {
effect.call(elem, state);
}
// If no effect type is supplied, use a simple toggle
else if(effect === FALSE) {
elem[ type ]();
}
// Use basic fade function
else {
elem.fadeTo( parseInt(duration, 10) || 90, state ? 1 : 0, function() {
if(!state) { elem.hide(); }
});
}
// Reset position and detach from body on hide
if(!state) {
elem.queue(function(next) {
elem.css({ left: '', top: '' });
if(!$(MODALSELECTOR).length) { elem.detach(); }
next();
});
}
// Cache the state
prevState = state;
// If the tooltip is destroyed, set reference to null
if(current.destroyed) { current = NULL; }
return self;
}
});
self.init();
};
OVERLAY = new OVERLAY();
function Modal(api, options) {
this.options = options;
this._ns = '-modal';
this.qtiptisas = api;
this.init(api);
}
$.extend(Modal.prototype, {
init: function(qtiptisas) {
var tooltip = qtiptisas.tooltip;
// If modal is disabled... return
if(!this.options.on) { return this; }
// Set overlay reference
qtiptisas.elements.overlay = OVERLAY.elem;
// Add unique attribute so we can grab modal tooltips easily via a SELECTOR, and set z-index
tooltip.addClass(MODALCLASS).css('z-index', QTIPTISAS.modal_zindex + $(MODALSELECTOR).length);
// Apply our show/hide/focus modal events
qtiptisas._bind(tooltip, ['tooltipshow', 'tooltiphide'], function(event, api, duration) {
var oEvent = event.originalEvent;
// Make sure mouseout doesn't trigger a hide when showing the modal and mousing onto backdrop
if(event.target === tooltip[0]) {
if(oEvent && event.type === 'tooltiphide' && /mouse(leave|enter)/.test(oEvent.type) && $(oEvent.relatedTarget).closest(OVERLAY.elem[0]).length) {
/* eslint-disable no-empty */
try { event.preventDefault(); }
catch(e) {}
/* eslint-enable no-empty */
}
else if(!oEvent || oEvent && oEvent.type !== 'tooltipsolo') {
this.toggle(event, event.type === 'tooltipshow', duration);
}
}
}, this._ns, this);
// Adjust modal z-index on tooltip focus
qtiptisas._bind(tooltip, 'tooltipfocus', function(event, api) {
// If focus was cancelled before it reached us, don't do anything
if(event.isDefaultPrevented() || event.target !== tooltip[0]) { return; }
var qtiptisass = $(MODALSELECTOR),
// Keep the modal's lower than other, regular qtiptisass
newIndex = QTIPTISAS.modal_zindex + qtiptisass.length,
curIndex = parseInt(tooltip[0].style.zIndex, 10);
// Set overlay z-index
OVERLAY.elem[0].style.zIndex = newIndex - 1;
// Reduce modal z-index's and keep them properly ordered
qtiptisass.each(function() {
if(this.style.zIndex > curIndex) {
this.style.zIndex -= 1;
}
});
// Fire blur event for focused tooltip
qtiptisass.filter('.' + CLASS_FOCUS).qtiptisas('blur', event.originalEvent);
// Set the new z-index
tooltip.addClass(CLASS_FOCUS)[0].style.zIndex = newIndex;
// Set current
OVERLAY.update(api);
// Prevent default handling
/* eslint-disable no-empty */
try { event.preventDefault(); }
catch(e) {}
/* eslint-enable no-empty */
}, this._ns, this);
// Focus any other visible modals when this one hides
qtiptisas._bind(tooltip, 'tooltiphide', function(event) {
if(event.target === tooltip[0]) {
$(MODALSELECTOR).filter(':visible').not(tooltip).last().qtiptisas('focus', event);
}
}, this._ns, this);
},
toggle: function(event, state, duration) {
// Make sure default event hasn't been prevented
if(event && event.isDefaultPrevented()) { return this; }
// Toggle it
OVERLAY.toggle(this.qtiptisas, !!state, duration);
},
destroy: function() {
// Remove modal class
this.qtiptisas.tooltip.removeClass(MODALCLASS);
// Remove bound events
this.qtiptisas._unbind(this.qtiptisas.tooltip, this._ns);
// Delete element reference
OVERLAY.toggle(this.qtiptisas, FALSE);
delete this.qtiptisas.elements.overlay;
}
});
MODAL = PLUGINS.modal = function(api) {
return new Modal(api, api.options.show.modal);
};
// Setup sanitiztion rules
MODAL.sanitize = function(opts) {
if(opts.show) {
if(typeof opts.show.modal !== 'object') { opts.show.modal = { on: !!opts.show.modal }; }
else if(typeof opts.show.modal.on === 'undefined') { opts.show.modal.on = TRUE; }
}
};
// Base z-index for all modal tooltips (use qTipTisas core z-index as a base)
/* eslint-disable camelcase */
QTIPTISAS.modal_zindex = QTIPTISAS.zindex - 200;
/* eslint-enable camelcase */
// Plugin needs to be initialized on render
MODAL.initialize = 'render';
// Setup option set checks
CHECKS.modal = {
'^show.modal.(on|blur)$': function() {
// Initialise
this.destroy();
this.init();
// Show the modal if not visible already and tooltip is visible
this.qtiptisas.elems.overlay.toggle(
this.qtiptisas.tooltip[0].offsetWidth > 0
);
}
};
// Extend original api defaults
$.extend(TRUE, QTIPTISAS.defaults, {
show: {
modal: {
on: FALSE,
effect: TRUE,
blur: TRUE,
stealfocus: TRUE,
escape: TRUE
}
}
});
;PLUGINS.viewport = function(api, position, posOptions, targetWidth, targetHeight, elemWidth, elemHeight)
{
var target = posOptions.target,
tooltip = api.elements.tooltip,
my = posOptions.my,
at = posOptions.at,
adjust = posOptions.adjust,
method = adjust.method.split(' '),
methodX = method[0],
methodY = method[1] || method[0],
viewport = posOptions.viewport,
container = posOptions.container,
adjusted = { left: 0, top: 0 },
fixed, newMy, containerOffset, containerStatic,
viewportWidth, viewportHeight, viewportScroll, viewportOffset;
// If viewport is not a jQuery element, or it's the window/document, or no adjustment method is used... return
if(!viewport.jquery || target[0] === window || target[0] === document.body || adjust.method === 'none') {
return adjusted;
}
// Cach container details
containerOffset = container.offset() || adjusted;
containerStatic = container.css('position') === 'static';
// Cache our viewport details
fixed = tooltip.css('position') === 'fixed';
viewportWidth = viewport[0] === window ? viewport.width() : viewport.outerWidth(FALSE);
viewportHeight = viewport[0] === window ? viewport.height() : viewport.outerHeight(FALSE);
viewportScroll = { left: fixed ? 0 : viewport.scrollLeft(), top: fixed ? 0 : viewport.scrollTop() };
viewportOffset = viewport.offset() || adjusted;
// Generic calculation method
function calculate(side, otherSide, type, adjustment, side1, side2, lengthName, targetLength, elemLength) {
var initialPos = position[side1],
mySide = my[side],
atSide = at[side],
isShift = type === SHIFT,
myLength = mySide === side1 ? elemLength : mySide === side2 ? -elemLength : -elemLength / 2,
atLength = atSide === side1 ? targetLength : atSide === side2 ? -targetLength : -targetLength / 2,
sideOffset = viewportScroll[side1] + viewportOffset[side1] - (containerStatic ? 0 : containerOffset[side1]),
overflow1 = sideOffset - initialPos,
overflow2 = initialPos + elemLength - (lengthName === WIDTH ? viewportWidth : viewportHeight) - sideOffset,
offset = myLength - (my.precedance === side || mySide === my[otherSide] ? atLength : 0) - (atSide === CENTER ? targetLength / 2 : 0);
// shift
if(isShift) {
offset = (mySide === side1 ? 1 : -1) * myLength;
// Adjust position but keep it within viewport dimensions
position[side1] += overflow1 > 0 ? overflow1 : overflow2 > 0 ? -overflow2 : 0;
position[side1] = Math.max(
-containerOffset[side1] + viewportOffset[side1],
initialPos - offset,
Math.min(
Math.max(
-containerOffset[side1] + viewportOffset[side1] + (lengthName === WIDTH ? viewportWidth : viewportHeight),
initialPos + offset
),
position[side1],
// Make sure we don't adjust complete off the element when using 'center'
mySide === 'center' ? initialPos - myLength : 1E9
)
);
}
// flip/flipinvert
else {
// Update adjustment amount depending on if using flipinvert or flip
adjustment *= type === FLIPINVERT ? 2 : 0;
// Check for overflow on the left/top
if(overflow1 > 0 && (mySide !== side1 || overflow2 > 0)) {
position[side1] -= offset + adjustment;
newMy.invert(side, side1);
}
// Check for overflow on the bottom/right
else if(overflow2 > 0 && (mySide !== side2 || overflow1 > 0) ) {
position[side1] -= (mySide === CENTER ? -offset : offset) + adjustment;
newMy.invert(side, side2);
}
// Make sure we haven't made things worse with the adjustment and reset if so
if(position[side1] < viewportScroll[side1] && -position[side1] > overflow2) {
position[side1] = initialPos; newMy = my.clone();
}
}
return position[side1] - initialPos;
}
// Set newMy if using flip or flipinvert methods
if(methodX !== 'shift' || methodY !== 'shift') { newMy = my.clone(); }
// Adjust position based onviewport and adjustment options
adjusted = {
left: methodX !== 'none' ? calculate( X, Y, methodX, adjust.x, LEFT, RIGHT, WIDTH, targetWidth, elemWidth ) : 0,
top: methodY !== 'none' ? calculate( Y, X, methodY, adjust.y, TOP, BOTTOM, HEIGHT, targetHeight, elemHeight ) : 0,
my: newMy
};
return adjusted;
};
;PLUGINS.polys = {
// POLY area coordinate calculator
// Special thanks to Ed Cradock for helping out with this.
// Uses a binary search algorithm to find suitable coordinates.
polygon: function(baseCoords, corner) {
var result = {
width: 0, height: 0,
position: {
top: 1e10, right: 0,
bottom: 0, left: 1e10
},
adjustable: FALSE
},
i = 0, next,
coords = [],
compareX = 1, compareY = 1,
realX = 0, realY = 0,
newWidth, newHeight;
// First pass, sanitize coords and determine outer edges
i = baseCoords.length;
while(i--) {
next = [ parseInt(baseCoords[--i], 10), parseInt(baseCoords[i+1], 10) ];
if(next[0] > result.position.right){ result.position.right = next[0]; }
if(next[0] < result.position.left){ result.position.left = next[0]; }
if(next[1] > result.position.bottom){ result.position.bottom = next[1]; }
if(next[1] < result.position.top){ result.position.top = next[1]; }
coords.push(next);
}
// Calculate height and width from outer edges
newWidth = result.width = Math.abs(result.position.right - result.position.left);
newHeight = result.height = Math.abs(result.position.bottom - result.position.top);
// If it's the center corner...
if(corner.abbrev() === 'c') {
result.position = {
left: result.position.left + result.width / 2,
top: result.position.top + result.height / 2
};
}
else {
// Second pass, use a binary search algorithm to locate most suitable coordinate
while(newWidth > 0 && newHeight > 0 && compareX > 0 && compareY > 0)
{
newWidth = Math.floor(newWidth / 2);
newHeight = Math.floor(newHeight / 2);
if(corner.x === LEFT){ compareX = newWidth; }
else if(corner.x === RIGHT){ compareX = result.width - newWidth; }
else{ compareX += Math.floor(newWidth / 2); }
if(corner.y === TOP){ compareY = newHeight; }
else if(corner.y === BOTTOM){ compareY = result.height - newHeight; }
else{ compareY += Math.floor(newHeight / 2); }
i = coords.length;
while(i--)
{
if(coords.length < 2){ break; }
realX = coords[i][0] - result.position.left;
realY = coords[i][1] - result.position.top;
if(
corner.x === LEFT && realX >= compareX ||
corner.x === RIGHT && realX <= compareX ||
corner.x === CENTER && (realX < compareX || realX > result.width - compareX) ||
corner.y === TOP && realY >= compareY ||
corner.y === BOTTOM && realY <= compareY ||
corner.y === CENTER && (realY < compareY || realY > result.height - compareY)) {
coords.splice(i, 1);
}
}
}
result.position = { left: coords[0][0], top: coords[0][1] };
}
return result;
},
rect: function(ax, ay, bx, by) {
return {
width: Math.abs(bx - ax),
height: Math.abs(by - ay),
position: {
left: Math.min(ax, bx),
top: Math.min(ay, by)
}
};
},
_angles: {
tc: 3 / 2, tr: 7 / 4, tl: 5 / 4,
bc: 1 / 2, br: 1 / 4, bl: 3 / 4,
rc: 2, lc: 1, c: 0
},
ellipse: function(cx, cy, rx, ry, corner) {
var c = PLUGINS.polys._angles[ corner.abbrev() ],
rxc = c === 0 ? 0 : rx * Math.cos( c * Math.PI ),
rys = ry * Math.sin( c * Math.PI );
return {
width: rx * 2 - Math.abs(rxc),
height: ry * 2 - Math.abs(rys),
position: {
left: cx + rxc,
top: cy + rys
},
adjustable: FALSE
};
},
circle: function(cx, cy, r, corner) {
return PLUGINS.polys.ellipse(cx, cy, r, r, corner);
}
};
;PLUGINS.svg = function(api, svg, corner)
{
var elem = svg[0],
root = $(elem.ownerSVGElement),
ownerDocument = elem.ownerDocument,
strokeWidth2 = (parseInt(svg.css('stroke-width'), 10) || 0) / 2,
frameOffset, mtx, transformed,
len, next, i, points,
result, position;
// Ascend the parentNode chain until we find an element with getBBox()
while(!elem.getBBox) { elem = elem.parentNode; }
if(!elem.getBBox || !elem.parentNode) { return FALSE; }
// Determine which shape calculation to use
switch(elem.nodeName) {
case 'ellipse':
case 'circle':
result = PLUGINS.polys.ellipse(
elem.cx.baseVal.value,
elem.cy.baseVal.value,
(elem.rx || elem.r).baseVal.value + strokeWidth2,
(elem.ry || elem.r).baseVal.value + strokeWidth2,
corner
);
break;
case 'line':
case 'polygon':
case 'polyline':
// Determine points object (line has none, so mimic using array)
points = elem.points || [
{ x: elem.x1.baseVal.value, y: elem.y1.baseVal.value },
{ x: elem.x2.baseVal.value, y: elem.y2.baseVal.value }
];
for(result = [], i = -1, len = points.numberOfItems || points.length; ++i < len;) {
next = points.getItem ? points.getItem(i) : points[i];
result.push.apply(result, [next.x, next.y]);
}
result = PLUGINS.polys.polygon(result, corner);
break;
// Unknown shape or rectangle? Use bounding box
default:
result = elem.getBBox();
result = {
width: result.width,
height: result.height,
position: {
left: result.x,
top: result.y
}
};
break;
}
// Shortcut assignments
position = result.position;
root = root[0];
// Convert position into a pixel value
if(root.createSVGPoint) {
mtx = elem.getScreenCTM();
points = root.createSVGPoint();
points.x = position.left;
points.y = position.top;
transformed = points.matrixTransform( mtx );
position.left = transformed.x;
position.top = transformed.y;
}
// Check the element is not in a child document, and if so, adjust for frame elements offset
if(ownerDocument !== document && api.position.target !== 'mouse') {
frameOffset = $((ownerDocument.defaultView || ownerDocument.parentWindow).frameElement).offset();
if(frameOffset) {
position.left += frameOffset.left;
position.top += frameOffset.top;
}
}
// Adjust by scroll offset of owner document
ownerDocument = $(ownerDocument);
position.left += ownerDocument.scrollLeft();
position.top += ownerDocument.scrollTop();
return result;
};
;PLUGINS.imagemap = function(api, area, corner)
{
if(!area.jquery) { area = $(area); }
var shape = (area.attr('shape') || 'rect').toLowerCase().replace('poly', 'polygon'),
image = $('img[usemap="#'+area.parent('map').attr('name')+'"]'),
coordsString = $.trim(area.attr('coords')),
coordsArray = coordsString.replace(/,$/, '').split(','),
imageOffset, coords, i, result, len;
// If we can't find the image using the map...
if(!image.length) { return FALSE; }
// Pass coordinates string if polygon
if(shape === 'polygon') {
result = PLUGINS.polys.polygon(coordsArray, corner);
}
// Otherwise parse the coordinates and pass them as arguments
else if(PLUGINS.polys[shape]) {
for(i = -1, len = coordsArray.length, coords = []; ++i < len;) {
coords.push( parseInt(coordsArray[i], 10) );
}
result = PLUGINS.polys[shape].apply(
this, coords.concat(corner)
);
}
// If no shapre calculation method was found, return false
else { return FALSE; }
// Make sure we account for padding and borders on the image
imageOffset = image.offset();
imageOffset.left += Math.ceil((image.outerWidth(FALSE) - image.width()) / 2);
imageOffset.top += Math.ceil((image.outerHeight(FALSE) - image.height()) / 2);
// Add image position to offset coordinates
result.position.left += imageOffset.left;
result.position.top += imageOffset.top;
return result;
};
;var IE6,
/*
* BGIFrame adaption (http://plugins.jquery.com/project/bgiframe)
* Special thanks to Brandon Aaron
*/
BGIFRAME = '<iframe class="qtiptisas-bgiframe" frameborder="0" tabindex="-1" src="javascript:\'\';" ' +
' style="display:block; position:absolute; z-index:-1; filter:alpha(opacity=0); ' +
'-ms-filter:"progid:DXImageTransform.Microsoft.Alpha(Opacity=0)";"></iframe>';
function Ie6(api) {
this._ns = 'ie6';
this.qtiptisas = api;
this.init(api);
}
$.extend(Ie6.prototype, {
_scroll : function() {
var overlay = this.qtiptisas.elements.overlay;
overlay && (overlay[0].style.top = $(window).scrollTop() + 'px');
},
init: function(qtiptisas) {
var tooltip = qtiptisas.tooltip;
// Create the BGIFrame element if needed
if($('select, object').length < 1) {
this.bgiframe = qtiptisas.elements.bgiframe = $(BGIFRAME).appendTo(tooltip);
// Update BGIFrame on tooltip move
qtiptisas._bind(tooltip, 'tooltipmove', this.adjustBGIFrame, this._ns, this);
}
// redraw() container for width/height calculations
this.redrawContainer = $('<div/>', { id: NAMESPACE+'-rcontainer' })
.appendTo(document.body);
// Fixup modal plugin if present too
if( qtiptisas.elements.overlay && qtiptisas.elements.overlay.addClass('qtiptisasmodal-ie6fix') ) {
qtiptisas._bind(window, ['scroll', 'resize'], this._scroll, this._ns, this);
qtiptisas._bind(tooltip, ['tooltipshow'], this._scroll, this._ns, this);
}
// Set dimensions
this.redraw();
},
adjustBGIFrame: function() {
var tooltip = this.qtiptisas.tooltip,
dimensions = {
height: tooltip.outerHeight(FALSE),
width: tooltip.outerWidth(FALSE)
},
plugin = this.qtiptisas.plugins.tip,
tip = this.qtiptisas.elements.tip,
tipAdjust, offset;
// Adjust border offset
offset = parseInt(tooltip.css('borderLeftWidth'), 10) || 0;
offset = { left: -offset, top: -offset };
// Adjust for tips plugin
if(plugin && tip) {
tipAdjust = plugin.corner.precedance === 'x' ? [WIDTH, LEFT] : [HEIGHT, TOP];
offset[ tipAdjust[1] ] -= tip[ tipAdjust[0] ]();
}
// Update bgiframe
this.bgiframe.css(offset).css(dimensions);
},
// Max/min width simulator function
redraw: function() {
if(this.qtiptisas.rendered < 1 || this.drawing) { return this; }
var tooltip = this.qtiptisas.tooltip,
style = this.qtiptisas.options.style,
container = this.qtiptisas.options.position.container,
perc, width, max, min;
// Set drawing flag
this.qtiptisas.drawing = 1;
// If tooltip has a set height/width, just set it... like a boss!
if(style.height) { tooltip.css(HEIGHT, style.height); }
if(style.width) { tooltip.css(WIDTH, style.width); }
// Simulate max/min width if not set width present...
else {
// Reset width and add fluid class
tooltip.css(WIDTH, '').appendTo(this.redrawContainer);
// Grab our tooltip width (add 1 if odd so we don't get wrapping problems.. huzzah!)
width = tooltip.width();
if(width % 2 < 1) { width += 1; }
// Grab our max/min properties
max = tooltip.css('maxWidth') || '';
min = tooltip.css('minWidth') || '';
// Parse into proper pixel values
perc = (max + min).indexOf('%') > -1 ? container.width() / 100 : 0;
max = (max.indexOf('%') > -1 ? perc : 1 * parseInt(max, 10)) || width;
min = (min.indexOf('%') > -1 ? perc : 1 * parseInt(min, 10)) || 0;
// Determine new dimension size based on max/min/current values
width = max + min ? Math.min(Math.max(width, min), max) : width;
// Set the newly calculated width and remvoe fluid class
tooltip.css(WIDTH, Math.round(width)).appendTo(container);
}
// Set drawing flag
this.drawing = 0;
return this;
},
destroy: function() {
// Remove iframe
this.bgiframe && this.bgiframe.remove();
// Remove bound events
this.qtiptisas._unbind([window, this.qtiptisas.tooltip], this._ns);
}
});
IE6 = PLUGINS.ie6 = function(api) {
// Proceed only if the browser is IE6
return BROWSER.ie === 6 ? new Ie6(api) : FALSE;
};
IE6.initialize = 'render';
CHECKS.ie6 = {
'^content|style$': function() {
this.redraw();
}
};
;}));
}( window, document ));
// ==UserScript==
// @name Twitter Image Searches and Stuff
// @version 1.13
// @description Searches Danbooru database for tweet IDs, adds image search links, and highlights images based on Tweet favorites.
// @match https://twitter.com/*
// @downloadURL https://gist.githubusercontent.com/BrokenEagle/dd9445d7e83d136716bb742228e50259/raw/Twitter_Image_Searches_and_Stuff.user.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.1/jquery.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/jquery-ui.js
// @require https://gist.githubusercontent.com/BrokenEagle/dd9445d7e83d136716bb742228e50259/raw/qtip_tisas.js
// @require https://raw.githubusercontent.com/jeresig/jquery.hotkeys/0.2.0/jquery.hotkeys.js
// @require https://cdnjs.cloudflare.com/ajax/libs/validate.js/0.12.0/validate.min.js
// @require https://cdnjs.cloudflare.com/ajax/libs/localforage/1.5.2/localforage.min.js
// @require https://raw.githubusercontent.com/localForage/localForage-setItems/v1.3.0/dist/localforage-setitems.js
// @require https://raw.githubusercontent.com/eligrey/FileSaver.js/2.0.0/dist/FileSaver.min.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20190213/lib/debug.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20190213/lib/load.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20190213/lib/utility.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20190213/lib/storage.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20190213/lib/validate.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20190213/lib/concurrency.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20190213/lib/danbooru.js
// @require https://raw.githubusercontent.com/BrokenEagle/JavaScripts/20190213/lib/menu.js
// @resource jquery_ui_css https://cdnjs.cloudflare.com/ajax/libs/jqueryui/1.12.1/themes/base/jquery-ui.css
// @resource jquery_qtip_css https://gist.githubusercontent.com/BrokenEagle/dd9445d7e83d136716bb742228e50259/raw/qtip_tisas.css
// @grant GM_getResourceText
// @grant GM.xmlHttpRequest
// @connect donmai.us
// @connect twimg.com
// @connect githubusercontent.com
// @noframes
// ==/UserScript==
/****Global variables****/
//Variables for debug.js
JSPLib.debug.debug_console = true;
JSPLib.debug.pretext = "TISAS:";
JSPLib.debug.pretimer = "TISAS-";
JSPLib.debug.level = JSPLib.debug.INFO;
//Variables for load.js
const program_load_required_variables = ['window.jQuery','window.Danbooru'];
const program_load_required_selectors = [".ProfileSidebar--withLeftAlignment,.SidebarFilterModule,.dashboard-left,.DashboardProfileCard"];
//JSPLib variable
window.Danbooru = {};
//Need to fix this for JSPLib.menu
Danbooru.Utility = {};
//Main program variable
var TISAS;
//Regex that matches the prefix of all program cache data
const program_cache_regex = /^(tweet|post|tisas)-/;
//Regex that matches the prefix of all expiring program cache data
const prune_cache_regex = /^(post)-/;
//Cleanup task intervals
const program_expires = JSPLib.utility.one_week;
//For factory reset !!!These need to be set!!!
const localstorage_keys = [];
const program_reset_keys = {};
const score_levels = ['excellent','good','aboveavg','fair','belowavg','poor'];
const subdomains = ['danbooru','kagamihara','saitou','shima'];
const all_positions = ['above','below'];
//Main settings
const settings_config = {
URL_wildcards_enabled: {
default: false,
validate: (data)=>{return JSPLib.validate.isBoolean(data);},
hint: "Manual searches of URLs will use wildcards in the search. <b>Note:</b> This will make the search take longer or timeout."
},
recheck_interval: {
default: 10,
parse: parseInt,
validate: (data)=>{return Number.isInteger(data) && data >= 5;},
hint: "Number of minutes. Valid values: >= 5. How often to check post versions once up to date."
},
query_subdomain: {
allitems: subdomains,
default: ['danbooru'],
validate: (data)=>{return Array.isArray(data) && data.length === 1 && data.reduce((is_string,val)=>{return is_string && (typeof val === 'string') && subdomains.includes(val);},true);},
hint: "Select which subdomain of Danbooru to query from. <b>Note:</b> The chosen subdomain must be logged into or the script will fail to work."
},
confirm_delete_enabled: {
default: true,
validate: (data)=>{return JSPLib.validate.isBoolean(data);},
hint: "Prompt the user on deleting results from the database."
},
confirm_IQDB_enabled: {
default: true,
validate: (data)=>{return JSPLib.validate.isBoolean(data);},
hint: "Prompt the user on adding IQDB results to the database."
},
autosave_IQDB_enabled: {
default: false,
validate: (data)=>{return JSPLib.validate.isBoolean(data);},
hint: "Add any IQDB results to the database automatically."
},
autocheck_IQDB_enabled: {
default: false,
validate: (data)=>{return JSPLib.validate.isBoolean(data);},
hint: "Will trigger the <b>Check IQDB</b> link if no results are found with the <b>Check URL</b> link."
},
auto_unhide_tweets_enabled: {
default: false,
validate: (data)=>{return JSPLib.validate.isBoolean(data);},
hint: "Automatically unhides sensitive Tweet content."
},
display_retweet_id: {
default: false,
validate: (data)=>{return JSPLib.validate.isBoolean(data);},
hint: "Displays the retweet ID next to the retweeter's name."
},
score_highlights_enabled: {
default: true,
validate: (data)=>{return JSPLib.validate.isBoolean(data);},
hint: "Adds colored borders and other stylings based upon the Tweet score."
},
advanced_tooltips_enabled: {
default: true,
validate: (data)=>{return JSPLib.validate.isBoolean(data);},
hint: "Displays extra information and thumbnails on IQDB results."
},
score_levels_faded: {
allitems: score_levels,
default: ['belowavg','poor'],
validate: (data)=>{return Array.isArray(data) && data.reduce((is_string,val)=>{return is_string && (typeof val === 'string') && score_levels.includes(val);},true);},
hint: "Select which score levels get faded automatically."
},
score_levels_hidden: {
allitems: score_levels,
default: ['poor'],
validate: (data)=>{return Array.isArray(data) && data.reduce((is_string,val)=>{return is_string && (typeof val === 'string') && score_levels.includes(val);},true);},
hint: "Select which score levels get hidden automatically."
},
score_window_size: {
default: 40,
parse: parseInt,
validate: (data)=>{return Number.isInteger(data) && data >= 10;},
hint: "Valid values: >= 10. The number of surrounding tweets to consider when calculating levels."
},
original_download_enabled: {
default: false,
validate: (data)=>{return JSPLib.validate.isBoolean(data);},
hint: "Shows download links for the original images on the Tweet view with customizable filename prefixes."
},
download_position: {
allitems: all_positions,
default: ['above'],
validate: (data)=>{return Array.isArray(data) && data.length === 1 && data.reduce((is_string,val)=>{return is_string && (typeof val === 'string') && all_positions.includes(val);},true);},
hint: "Select whether the download image links will appear above or below the images."
},
filename_prefix_format: {
default: "%TWEETID%--%IMG%",
parse: String,
validate: (data)=>{return JSPLib.validate.isString(data);},
hint: `Prefix to add to original image downloads. Available format keywords include:<br><span style="font-family:monospace;background:#eee">%TWEETID%, %USERID%, %USERACCOUNT%, %IMG%, %DATE%, %TIME%, %ORDER%</span>.`
}
}
//CSS constants
const program_css = `
.tisas-highlight.tisas-excellent .tweet {
border: red solid 10px;
}
.tisas-highlight.tisas-good .tweet {
border: orange solid 10px;
}
.tisas-highlight.tisas-aboveavg .tweet {
border: green solid 10px;
}
.tisas-highlight.tisas-fair .tweet {
border: blue solid 10px;
}
.tisas-highlight.tisas-belowavg .tweet {
border: purple solid 10px;
}
.tisas-highlight.tisas-poor .tweet {
border: black solid 10px;
}
.tisas-highlight.tisas-fade .tweet {
opacity: 0.2;
}
.tisas-highlight.tisas-fade .tweet:hover {
opacity: 1.0;
}
.tisas-highlight.tisas-hide .tweet {
max-height: 10px;
overflow: hidden;
}
.tisas-highlight.tisas-hide .tweet:hover {
max-height: unset;
}
`;
const menu_css = `
.jsplib-outer-menu {
float: left;
width: 100%;
min-width: 60em;
}
.jsplib-settings-grouping {
margin-bottom: 2em;
}
.jsplib-settings-buttons {
margin-top: 1em;
}
.jsplib-menu-item {
margin: 0.5em;
}
.jsplib-menu-item > div,
.jsplib-menu-item > ul {
margin-left: 0.5em;
}
.jsplib-inline-tooltip {
display: inline;
font-style: italic;
color: #666;
}
.jsplib-block-tooltip {
display: block;
font-style: italic;
color: #666;
}
.jsplib-textinput jsplib-setting {
padding: 1px 0.5em;
}
.jsplib-sortlist li {
width: 5.5em;
font-size: 125%;
}
.jsplib-sortlist li > div {
padding: 5px;
}
.jsplib-textinput-control .jsplib-control {
padding: 1px 0.5em;
}
.jsplib-selectors label {
width: 100px;
}
.jsplib-linkclick .jsplib-control {
display: inline;
}
.jsplib-console {
width: 100%;
min-width: 60em;
}
.jsplib-console hr,
.jsplib-console .expandable {
width: 90%;
margin-left: 0;
}
#twitter-image-searches-and-stuff {
z-index: 1001;
}
#twitter-image-searches-and-stuff p {
margin-bottom: 1em;
}
#twitter-image-searches-and-stuff h4 {
font-size: 1.16667em;
line-height: 1.5em;
}
#twitter-image-searches-and-stuff .prose h2 {
font-size: 1.8em;
padding: .8em 0 .25em;
line-height: 1em;
color: black;
}
#twitter-image-searches-and-stuff .prose h4 {
font-size: 1.4em;
padding: .8em 0 .25em;
}
#twitter-image-searches-and-stuff a {
color:#0073ff
}
#twitter-image-searches-and-stuff b {
font-weight: bold;
}
.tisas-textinput input {
width: unset;
}
#tisas-import-file,
#tisas-settings-buttons input,
.tisas-textinput input{
background-color: white;
}
`;
//HTML constants
const tisas_menu = `
<div id="tisas-script-message" class="prose">
<h2>Twitter Image Searches and Stuff</h2>
<p>Check the forum for the latest on information and updates (to be added).</p>
</div>
<div id="tisas-console" class="jsplib-console">
<div id="tisas-settings" class="jsplib-outer-menu">
<div id="tisas-display-settings" class="jsplib-settings-grouping">
<div id="tisas-display-message" class="prose">
<h4>Display settings</h4>
</div>
</div>
<div id="tisas-highlight-settings" class="jsplib-settings-grouping">
<div id="tisas-highlight-message" class="prose">
<h4>Highlight settings</h4>
</div>
</div>
<div id="tisas-database-settings" class="jsplib-settings-grouping">
<div id="tisas-database-message" class="prose">
<h4>Database settings</h4>
</div>
</div>
<div id="tisas-network-settings" class="jsplib-settings-grouping">
<div id="tisas-network-message" class="prose">
<h4>Network settings</h4>
</div>
</div>
<div id="tisas-download-settings" class="jsplib-settings-grouping">
<div id="tisas-download-message" class="prose">
<h4>Download settings</h4>
</div>
</div>
<div id="tisas-cache-settings" class="jsplib-settings-grouping">
<div id="tisas-cache-message" class="prose">
<h4>Cache settings</h4>
</div>
</div>
<hr>
<div id="tisas-settings-buttons" class="jsplib-settings-buttons">
<input type="button" id="tisas-commit" value="Save">
<input type="button" id="tisas-resetall" value="Factory Reset">
</div>
</div>
</div>`;
const notice_banner = '<div id="notice" style="padding: .25em;position: fixed;top: 4em;left: 25%;width: 50%;z-index: 1002;display: none"><span>.</span><a href="#" id="close-notice-link" style="right:1em;position:absolute;color:#0073ff">close</a></div>';
const load_counter = '<span id="tisas-load-message">Loading ( <span id="tisas-counter">...</span> )</span>';
//Database constants
const server_database_url = "https://gist.githubusercontent.com/BrokenEagle/53a6ec5d343741fc80540b40340ca782/raw/tweetdict.json";
const database_info_url = "https://gist.githubusercontent.com/BrokenEagle/53a6ec5d343741fc80540b40340ca782/raw/database_info.json";
//Time constants
const timer_poll_interval = 100;
const program_recheck_interval = 1000;
const post_versions_callback = 5000;
//Other constants
const query_limit = 100;
const query_batch_size = 499;
const twitter_regex = /^https:\/\/twitter\.com\/[\w-]+\/status\/(\d+)$/;
//Qtip constants
const ARTIST_QTIP_SETTINGS = {
style: {
classes: "qtiptisas-light tisas-iqdb-tooltip",
},
position: {
my: "top center",
at: "bottom center",
viewport: true,
},
show: {
delay: 150,
solo: true,
},
hide: {
delay: 250,
fixed: true,
}
};
/****jQuery Setup****/
// https://gist.github.com/monperrus/999065
// This is a shim that adapts jQuery's ajax methods to use GM_xmlhttpRequest.
// This allows the use $.getJSON instead of using GM_xmlhttpRequest directly.
//
// This is necessary because some sites have a Content Security Policy (CSP) which
// blocks cross-origin requests to Danbooru that require authentication.
// Tampermonkey can bypass the CSP, but only if GM_xmlhttpRequest is used.
function GM_XHR() {
const open_params = ['type','url','async','username','password'];
Object.assign(this,{headers: {}},...open_params.concat(['status','readyState']).map(function (key) {return {[key]: null};}));
this.abort = function() {
this.readyState = 0;
};
this.getAllResponseHeaders = function(name) {
return (this.readyState != 4 ? "" : this.responseHeaders);
};
this.getResponseHeader = function(name) {
var regexp = new RegExp('^'+name+': (.*)$','im');
var match = regexp.exec(this.responseHeaders);
return (match ? match[1] : '');
};
this.open = function(type, url, async, username, password) {
let outerargs = arguments;
let xhr = this;
open_params.forEach(function (arg,i) {
xhr[arg] = outerargs[i] || null;
});
this.readyState = 1;
};
this.setRequestHeader = function(name, value) {
this.headers[name] = value;
};
this.onresponse = function (handler) {
let xhr = this;
return function (resp) {
['readyState','responseHeaders','responseText','status','statusText'].forEach(function (key) {
xhr[key] = resp[key];
});
if (xhr[handler]) {
xhr[handler].call(xhr);
} else {
xhr.onreadystatechange();
}
};
};
this.send = function(data) {
this.data = data;
GM.xmlHttpRequest({
method: this.type,
url: this.url,
headers: this.headers,
data: this.data,
responseType: this.responseType,
onload: this.onresponse("onload"),
onerror: this.onresponse("onerror"),
});
};
}
$.ajaxSetup({xhr: function () {return new GM_XHR();}});
/****Functions****/
const post_constraints = {
entry: JSPLib.validate.hashentry_constraints,
value: {
uploader: JSPLib.validate.stringonly_constraints,
score: JSPLib.validate.integer_constraints,
favcount: JSPLib.validate.counting_constraints,
rating: JSPLib.validate.inclusion_constraints(['s','q','e']),
tags: JSPLib.validate.stringonly_constraints
}
};
function ValidateEntry(key,entry) {
if (!JSPLib.validate.validateIsHash(key, entry)) {
return false;
}
if (key.match(/^post-/)) {
return ValidatePostEntry(key, entry);
}
ValidateEntry.debuglog("Bad key!");
return false;
}
function ValidatePostEntry(key,entry) {
if (!JSPLib.validate.validateHashEntries(key, entry, post_constraints.entry)) {
return false;
}
return JSPLib.validate.validateHashEntries(key + '.value', entry.value, post_constraints.value);
}
function ValidateProgramData(key,entry) {
var checkerror=[];
switch (key) {
case 'tisas-recent-timestamp':
if (!Number.isInteger(entry)) {
checkerror = ["Value is not an integer."];
}
break;
case 'tisas-postver-lastid':
if (!JSPLib.validate.validateID(entry)) {
checkerror = ["Value is not a valid ID."];
}
break;
case 'tisas-overflow':
if (!JSPLib.validate.isBoolean(entry)) {
checkerror = ["Value is not a boolean."];
}
break;
default:
checkerror = ["Not a valid program data key."];
}
if (checkerror.length) {
JSPLib.validate.printValidateError(key,checkerror);
return false;
}
return true;
}
function CorrectArtistList(artistlist) {
let error_messages = [];
if (!Array.isArray(artistlist)) {
JSPLib.debug.debuglog("Value is not an array.");
return [];
}
let correctlist = artistlist.filter((name)=>{return JSPLib.validate.isString(name);});
if (artistlist.length !== correctlist.length) {
JSPLib.storage.setStorageData('tisas-no-highlight-list',correctlist,localStorage);
JSPLib.debug.debugExecute(()=>{
let bad_values = JSPLib.utility.setDifference(artistlist,correctlist);
JSPLib.debug.debuglog("Bad values found:",bad_values);
});
}
return correctlist;
}
//Library functions
Danbooru.Utility.notice = function(msg, permanent) {
$('#notice').addClass("ui-state-highlight").removeClass("ui-state-error").fadeIn("fast").children("span").html(msg);
if (Danbooru.Utility.notice_timeout_id !== undefined) {
clearTimeout(Danbooru.Utility.notice_timeout_id);
}
if (!permanent) {
Danbooru.Utility.notice_timeout_id = setTimeout(function() {
$("#close-notice-link").click();
Danbooru.Utility.notice_timeout_id = undefined;
}, 6000);
}
};
Danbooru.Utility.error = function(msg) {
$('#notice').removeClass("ui-state-highlight").addClass("ui-state-error").fadeIn("fast").children("span").html(msg);
if (Danbooru.Utility.notice_timeout_id !== undefined) {
clearTimeout(Danbooru.Utility.notice_timeout_id);
}
};
Danbooru.Utility.closeNotice = function (event) {
$('#notice').fadeOut("fast");
event.preventDefault();
};
////Fixes for validate.js
JSPLib.validate.isHash = function (value) {
return typeof value === "object" && value !== null && !Array.isArray(value);
};
////Fixes for danbooru.js
JSPLib.danbooru.submitRequest = async function (type,url_addons,default_val=null,key,domain='',notify_user=false) {
key = key || JSPLib.danbooru.randomDummyTag();
if (JSPLib.danbooru.num_network_requests >= JSPLib.danbooru.max_network_requests) {
await JSPLib.danbooru.rateLimit();
}
JSPLib.danbooru.incrementCounter();
JSPLib.debug.recordTime(key,'Network');
try {
return await jQuery.getJSON(`${domain}/${type}.json`,url_addons
).always(()=>{
JSPLib.debug.recordTimeEnd(key,'Network');
JSPLib.danbooru.decrementCounter();
});
} catch(e) {
//Swallow exception... will return default value
e = (typeof e === "object" && 'status' in e && 'responseText' in e ? e : {status: 999, responseText: "Bad error code!"});
JSPLib.debug.debuglogLevel("SubmitRequest error:",e.status,e.responseText,JSPLib.debug.ERROR);
if (notify_user) {
let message = e.responseText;
try {
let parse_message = JSON.parse(message);
message = (JSPLib.validate.isHash(parse_message) && 'message' in parse_message ? parse_message.message : message);
} catch (e) {
//Swallow
}
Danbooru.Utility.error(`HTTP ${e.status}: ${message}`);
}
return default_val;
}
};
JSPLib.danbooru.getAllItems = async function (type,limit,options) {
let url_addons = options.addons || {};
let reverse = options.reverse || false;
let page_modifier = (reverse ? 'a' : 'b');
let domain = options.domain;
let page_addon = (options.page ? {page:`${page_modifier}${options.page}`} : {});
let limit_addon = {limit: limit};
var return_items = [];
while (true) {
let request_addons = JSPLib.danbooru.joinArgs(url_addons,page_addon,limit_addon);
let request_key = jQuery.param(request_addons);
let temp_items = await JSPLib.danbooru.submitRequest(type,request_addons,[],request_key,domain);
return_items = return_items.concat(temp_items);
if (temp_items.length < limit) {
return return_items;
}
let lastid = JSPLib.danbooru.getNextPageID(temp_items,reverse);
page_addon = {page:`${page_modifier}${lastid}`};
JSPLib.debug.debuglogLevel("Rechecking",type,"@",lastid,JSPLib.debug.INFO);
}
};
////New for utility
JSPLib.utility.joinList = function (array,prefix,joiner) {
return array.map((level)=>{return prefix + level;}).join(joiner);
}
JSPLib.utility.parseParams = function (str) {
return str.split('&').reduce(function (params, param) {
var paramSplit = param.split('=').map(function (value) {
return decodeURIComponent(value.replace(/\+/g, ' '));
});
params[paramSplit[0]] = paramSplit[1];
return params;
}, {});
};
//Helper functions
function PadNumber(num,size) {
var s = String(num);
while (s.length < (size || 2)) {s = "0" + s;}
return s;
}
function GetNumericTimestamp(timestamp) {
let time_obj = new Date(timestamp);
return GetDateString(timestamp) + GetTimeString(timestamp);
}
function GetDateString(timestamp) {
let time_obj = new Date(timestamp);
return `${time_obj.getFullYear()}${PadNumber(time_obj.getMonth()+1,2)}${PadNumber(time_obj.getDate(),2)}`;
}
function GetTimeString(timestamp) {
let time_obj = new Date(timestamp);
return `${PadNumber(time_obj.getHours(),2)}${PadNumber(time_obj.getMinutes(),2)}`;
}
function ParseQueries(str) {
return str.split(' ').reduce(function (params, param) {
var paramSplit = param.split(':');
params[paramSplit[0]] = paramSplit[1];
return params;
}, {});
}
function TimeAgo(timestamp) {
let time_interval = Date.now() - timestamp;
if (time_interval < GetPostVersionsExpiration() * 2) {
return "Up to date";
} else if (time_interval < JSPLib.utility.one_hour) {
return JSPLib.utility.setPrecision(time_interval / JSPLib.utility.one_minute, 2) + " minutes ago";
} else if (time_interval < JSPLib.utility.one_day) {
return JSPLib.utility.setPrecision(time_interval / JSPLib.utility.one_hour, 2) + " hours ago";
} else if (time_interval < JSPLib.utility.one_month) {
return JSPLib.utility.setPrecision(time_interval / JSPLib.utility.one_day, 2) + " days ago";
} else {
return "> 1 month ago";
}
}
function GetPostVersionsExpiration() {
return TISAS.user_settings.recheck_interval * JSPLib.utility.one_minute;
}
function WasOverflow() {
return JSPLib.storage.checkStorageData('tisas-overflow',ValidateProgramData,localStorage,false);
}
function MapPostData(posts) {
return posts.map(MapPost);
}
function MapPost(post) {
return {
uploader: post.uploader_name,
score: post.score,
favcount: post.fav_count,
rating: post.rating,
tags: post.tag_string
}
}
function GetLinkTitle(post,is_render=true) {
let tags = post.tags;
if (is_render) {
tags = jQueryEscape(post.tags);
}
return `user:${post.uploader} score:${post.score} favcount:${post.favcount} rating:${post.rating} ${post.tags}`;
}
function jQueryEscape(string) {
return jQuery('<div />').attr('parse',string)[0].outerHTML.match(/parse="([^"]+)"/)[1];
}
function GetPostVersionsLastID() {
//Get the program last ID if it exists
let postver_lastid = JSPLib.storage.checkStorageData('tisas-postver-lastid',ValidateProgramData,localStorage,TISAS.database_info.post_version);
//Select the largest of the program lastid and the database lastid
let max_postver_lastid = Math.max(postver_lastid,TISAS.database_info.post_version);
if (postver_lastid !== max_postver_lastid) {
JSPLib.storage.setStorageData('tisas-postver-lastid',max_postver_lastid,localStorage);
}
return max_postver_lastid;
}
function GetPreviewDimensions(image_width,image_height) {
let scale = Math.min(150 / image_width, 150 / image_height);
scale = Math.min(1, scale);
let width = Math.round(image_width * scale);
let height = Math.round(image_height * scale);
return [width,height];
}
function ReadableBytes(bytes) {
var i = Math.floor(Math.log(bytes) / Math.log(1024)),
sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return JSPLib.utility.setPrecision((bytes / Math.pow(1024, i)),2) + ' ' + sizes[i];
}
function GetBaseDomainName(url) {
let parser = $("<a \>").attr('href',url)[0];
let root = parser.hostname.lastIndexOf('.');
let domain = parser.hostname.lastIndexOf('.', root - 1);
return parser.hostname.slice(domain + 1);
}
function GetFileExtension(url,splitter) {
let parser = $("<a \>").attr('href',url)[0];
let pathname = parser.pathname.split(splitter)[0];
let extpos = pathname.lastIndexOf('.');
return pathname.slice(extpos + 1);
}
function GetThumbUrl(url,splitter,ext) {
let parser = $("<a \>").attr('href',url)[0];
let pathname = parser.pathname.split(splitter)[0];
let extpos = pathname.lastIndexOf('.');
return parser.origin + pathname.slice(0, extpos + 1) + ext;
}
function RemoveDuplicates(obj_array, attribute){
const attribute_index = obj_array.map((item)=>{return item[attribute]});
return obj_array.filter((obj,index)=>{
return attribute_index.indexOf(obj[attribute]) === index;
});
}
//Auxiliary functions
function GetArtistList() {
let artist_list = JSPLib.storage.getStorageData('tisas-no-highlight-list',localStorage,[]);
return CorrectArtistList(artist_list);
}
function SetCheckPostVers() {
if (JSPLib.concurrency.checkTimeout('tisas-timeout',GetPostVersionsExpiration()) || WasOverflow()) {
clearTimeout(CheckPostvers.timeout);
CheckPostvers.timeout = setTimeout(()=>{CheckPostvers();},post_versions_callback);
}
}
function GetTweetQuartile(tweetid) {
if (tweetid in TISAS.tweet_finish) {
return TISAS.tweet_finish[tweetid];
}
let windowsize = TISAS.user_settings.score_window_size;
let pos = TISAS.tweet_pos.indexOf(tweetid);
let fave = TISAS.tweet_faves[pos];
let posmin = Math.max(0, pos - windowsize);
let posmax = Math.min(TISAS.tweet_pos.length, pos + windowsize);
let subarray = TISAS.tweet_faves.slice(posmin,posmax);
var quartilepos = Math.floor(subarray.length / 4);
var sortedfaves = subarray.sort((a,b)=>{return a - b;});
var q1 = sortedfaves[quartilepos];
var q2 = sortedfaves[2*quartilepos];
var q3 = sortedfaves[3*quartilepos];
var min = Math.min(...sortedfaves);
var max = Math.max(...sortedfaves);
var outlierq1 = (q1 + min) / 2;
var outlierq3 = (q3 + max) / 2;
let quartile = 5;
if (fave >= outlierq3) {
quartile = 0;
} else if (fave >= q3) {
quartile = 1;
} else if (fave >= q2) {
quartile = 2;
} else if (fave >= q1) {
quartile = 3;
} else if (fave >= outlierq1) {
quartile = 4;
}
if ((posmax - posmin) >= (windowsize * 2)) {
TISAS.tweet_finish[tweetid] = quartile;
}
return quartile;
}
function ProcessTweets($tweets,primaryfilter,append_selector,outerHTML) {
var all_post_ids = [];
var $filter_tweets = $tweets;
if (primaryfilter) {
$filter_tweets = $filter_tweets.filter(primaryfilter);
}
$filter_tweets = $filter_tweets.filter((i,tweet)=>{return $(tweet).find(append_selector).length;});
if ($filter_tweets.length === 0) {
return;
}
let tweet_ids = $filter_tweets.map((i,entry)=>{return $(entry).data('tweet-id');}).toArray();
JSPLib.debug.debuglog("Check Tweets:",tweet_ids);
let promise_array = tweet_ids.map((tweet_id)=>{return JSPLib.storage.retrieveData('tweet-' + tweet_id);});
Promise.all(promise_array).then((data_items)=>{
JSPLib.debug.debuglog("Tweet data:",data_items);
data_items.forEach((data,i)=>{
let postlink;
let tweet_id = tweet_ids[i];
let $tweet = $filter_tweets.filter(`[data-tweet-id=${tweet_id}]`);
if (data !== null) {
if (data.length === 1) {
postlink = `( <a style="color:green;font-weight:bold" target="_blank" href="${TISAS.domain}/posts/${data[0]}" class="tisas-post-callback tisas-confirm-delete">post #${data[0]}</a> )`
ProcessTweets.tweet_index[tweet_id] = {entry: $tweet, post_ids: data, processed: false};
all_post_ids = all_post_ids.concat(data);
} else {
postlink = `( <a style="color:green;font-weight:bold" target="_blank" href="${TISAS.domain}/posts?tags=status%3Aany+id%3A${data.join(',')}" class="tisas-post-callback tisas-confirm-delete">${data.length} sources</a> ) `;
}
} else {
postlink = `( <span style="color:red">no sources</span> | <a style="color:grey" href="#" class="tisas-check-url">Check URL</a> | <a style="color:grey" href="#" class="tisas-check-iqdb">Check IQDB</a> )</span>`;
$tweets.attr("tisas", "done");
}
let $link_container = $(outerHTML).append(postlink);
$tweet.find(append_selector).append($link_container);
});
if (all_post_ids.length) {
JSPLib.storage.batchStorageCheck(all_post_ids,ValidateEntry,JSPLib.utility.one_day,'post').then((missing_ids)=>{
JSPLib.debug.debuglog("Missing posts:",missing_ids);
if (missing_ids.length) {
JSPLib.danbooru.submitRequest('posts',{tags: 'id:' + missing_ids.join(','), limit: missing_ids.length},[],null,TISAS.domain).then((data)=>{
let mapped_data = MapPostData(data);
mapped_data.forEach((post,i)=>{
let post_id = data[i].id;
ProcessTweets.post_index[post_id] = post;
JSPLib.storage.saveData('post-' + post_id, {value: post, expires: JSPLib.utility.getExpiration(JSPLib.utility.one_day)});
});
UpdateLinkTitles();
});
}
let found_ids = JSPLib.utility.setDifference(all_post_ids,missing_ids.map(Number));
JSPLib.debug.debuglog("Found posts:",found_ids);
found_ids.forEach((post_id)=>{
ProcessTweets.post_index[post_id] = JSPLib.storage.getStorageData('post-' + post_id, sessionStorage).value;
});
UpdateLinkTitles();
});
}
});
}
ProcessTweets.tweet_index = {};
ProcessTweets.post_index = {};
function UpdateLinkTitles() {
for (let tweet_id in ProcessTweets.tweet_index) {
let tweet_entry = ProcessTweets.tweet_index[tweet_id];
if (tweet_entry.post_ids.length > 1 || tweet_entry.processed) {
continue;
}
let post_id = tweet_entry.post_ids[0];
if (post_id in ProcessTweets.post_index) {
let $link = tweet_entry.entry.find('.tisas-post-callback');
$link.attr('title',GetLinkTitle(ProcessTweets.post_index[post_id],false));
$link.removeClass("tisas-post-callback");
tweet_entry.processed = true;
}
}
}
function ReversalCheck(hash,key,id) {
if ((key in hash) && hash[key].includes(id)) {
hash[key] = JSPLib.utility.setDifference(hash[key],[id]);
if (hash[key].length === 0) {
delete hash[key];
}
return true;
}
return false;
}
function ProcessPostvers(postvers) {
postvers.sort((a,b)=>{return a.id - b.id;});
var account_swaps = 0;
var inactive_posts = 0;
var reversed_posts = 0;
var add_entries = {};
var rem_entries = {};
postvers.forEach((postver)=>{
if (postver.source_changed) {
if (postver.version === 1) {
let match = postver.source.match(twitter_regex);
if (match) {
add_entries[match[1]] = JSPLib.utility.setUnion(add_entries[match[1]] || [], [postver.post_id]);
}
} else {
let source_add = postver.added_tags.filter((tag)=>{return tag.replace(/^source:/,'').match(twitter_regex);}).map((tag)=>{return tag.replace(/^source:/,'').match(twitter_regex)[1];});
let source_rem = postver.removed_tags.filter((tag)=>{return tag.replace(/^source:/,'').match(twitter_regex);}).map((tag)=>{return tag.replace(/^source:/,'').match(twitter_regex)[1];});
if (source_add.length && source_rem.length) {
if (source_add[0] === source_rem[0]) {
source_add.length = source_rem.length = 0;
account_swaps++;
} else {
JSPLib.debug.debuglog("ID swap detected",source_rem[0],"->",source_add[0]);
}
}
if (source_add.length) {
add_entries[source_add[0]] = JSPLib.utility.setUnion(add_entries[source_add[0]] || [], [postver.post_id]);
if (ReversalCheck(rem_entries,source_add[0],postver.post_id)) {
JSPLib.debug.debuglog("Source delete reversal detected",source_add[0]);
}
}
if (source_rem.length) {
rem_entries[source_rem[0]] = JSPLib.utility.setUnion(rem_entries[source_rem[0]] || [], [postver.post_id]);
if (ReversalCheck(add_entries,source_rem[0],postver.post_id)) {
JSPLib.debug.debuglog("Source add reversal detected",source_rem[0]);
}
}
}
}
if (postver.added_tags.includes('bad_twitter_id') || postver.removed_tags.includes('bad_twitter_id')) {
let match = postver.source.match(twitter_regex);
if (match) {
if (postver.removed_tags.includes('bad_twitter_id')) {
JSPLib.debug.debuglog("Activated tweet:",match[1]);
add_entries[match[1]] = JSPLib.utility.setUnion(add_entries[match[1]] || [], [postver.post_id]);
reversed_posts++;
if (ReversalCheck(rem_entries,match[1],postver.post_id)) {
JSPLib.debug.debuglog("Tweet remove reversal detected",match[1]);
}
} else if (postver.added_tags.includes('bad_twitter_id')) {
rem_entries[match[1]] = JSPLib.utility.setUnion(rem_entries[match[1]] || [], [postver.post_id]);
inactive_posts++;
if (ReversalCheck(add_entries,match[1],postver.post_id)) {
JSPLib.debug.debuglog("Tweet add reversal detected",match[1]);
}
}
}
}
});
if (account_swaps > 0) {
JSPLib.debug.debuglog("Account swaps detected:",account_swaps);
}
if (inactive_posts > 0) {
JSPLib.debug.debuglog("Inactive tweets detected:",inactive_posts);
}
if (reversed_posts > 0) {
JSPLib.debug.debuglog("Activated tweets detected:",reversed_posts);
}
return [add_entries,rem_entries];
}
function GetPageType() {
const home_regex = "(^https:\/\/twitter\\.com\/?(?:\\?|$))";
const main_regex = "(^https:\/\/twitter\\.com\/([\\w-]+)(?:\\?|$))";
const likes_regex = "(^https:\/\/twitter\\.com\/([\\w-]+)\/likes(?:\\?|$))";
const replies_regex = "(^https:\/\/twitter\\.com\/([\\w-]+)\/with_replies(?:\\?|$))";
const media_regex = "(^https:\/\/twitter\\.com\/([\\w-]+)\/media(?:\\?|$))";
const list_regex = "(^https:\/\/twitter\\.com\/[\\w-]+\/lists\/([\\w-]+)(?:\\?|$))";
const hashtag_regex = "(^https:\/\/twitter\\.com\/hashtag\/(.+?)(?:\\?|$))";
const search_regex = "(^https:\/\/twitter\\.com\/search\\?(.*?\\bq=.+))";
const tweet_regex = "(^https:\/\/twitter\\.com\/[\\w-]+\/status\/(\\d+)(?:\\?|$))";
const page_regex = RegExp(`(${main_regex}|${media_regex}|${search_regex}|${tweet_regex}|${hashtag_regex}|${list_regex}|${home_regex}|${likes_regex}|${replies_regex})`);
let match = page_regex.exec(window.location.href);
if (!match) {
return [null,null];
}
switch(match[1]) {
case match[2]:
return ["main",match[3]];
case match[4]:
return ["media",match[5]];
case match[6]:
return ["search",match[7]];
case match[8]:
return ["tweet",match[9]];
case match[10]:
return ["hashtag",match[11]];
case match[12]:
return ["list",match[13]];
case match[14]:
return ["home",null];
case match[15]:
return ["likes",match[16]];
case match[17]:
return ["replies",match[18]];
default:
return [null,null];
}
}
function UpdateArtistHilights() {
if (TISAS.account) {
let no_highlight_list = GetArtistList();
if (no_highlight_list.includes(TISAS.account)) {
$("#tisas-enable-highlights").show();
$("#tisas-disable-highlights").hide();
} else {
$("#tisas-enable-highlights").hide();
$("#tisas-disable-highlights").show();
}
}
}
//File functions
function ReadFileAsync(fileselector,is_json) {
return new Promise((resolve,reject)=>{
var files = document.querySelector(fileselector).files;
if (!files.length) {
alert('Please select a file!');
reject();
return;
}
var file = files[0];
var start = 0;
var stop = file.size - 1;
var reader = new FileReader();
// If we use onloadend, we need to check the readyState.
reader.onloadend = function(event) {
if (event.target.readyState == FileReader.DONE) { // DONE == 2
JSPLib.debug.debuglog("File loaded:",file.size);
let data;
if (is_json) {
try {
data = JSON.parse(event.target.result);
} catch (e) {
JSPLib.debug.debuglog("Error: File is not JSON!");
reject();
}
} else {
data = event.target.result;
}
resolve(data);
}
};
var blob = file.slice(start, stop + 1);
reader.readAsBinaryString(blob);
});
}
function DownloadObject(export_obj, export_name, is_json) {
var export_data = export_obj;
var encoding = {type: "text/plain;charset=utf-8"};
if (is_json) {
export_data = JSON.stringify(export_obj);
encoding = {type: "text/json;charset=utf-8"};
}
var blob = new Blob([export_data], encoding);
saveAs(blob, export_name);
}
//Render functions
function RenderMenu() {
let artist_html = `
<span style="font-weight:bold">
<a href="#" id="tisas-enable-highlights" title="Click to enable Tweet hiding/fading. (Shortcut: Alt+H)" style="color:green;display:none">Enable</a>
<a href="#" id="tisas-disable-highlights" title="Click to disable Tweet hiding/fading. (Shortcut: Alt+H)" style="color:red;display:none">Disable</a>
</span>
`;
return `
<div id="tisas-side-menu" style="border:solid lightgrey 1px;height:10em">
<div style="margin:8px;font-size:18px;font-weight:bold;line-height:1;letter-spacing:-1px;text-decoration:underline">Twitter Image Searches and Stuff</div>
<ul style="margin-left:10px">
<li><span style="font-weight:bold;letter-spacing:-0.5px">Database version:</span> <span id="tisas-database-stub"></span></li>
<li><span style="font-weight:bold">Current records:</span> ${RenderCurrentRecords()}</li>
<li><span style="font-weight:bold">Artist highlights:</span> ${artist_html}</span></li>
</ul>
<div style="margin-left:0.5em">
<input type="button" id="tisas-settings" title="Click to open settings menu. (Shortcut: Alt+M)" value="Settings" style="font-weight:bold;width:19.5em">
</div>
</div>
`;
}
function RenderCurrentRecords() {
var record_html = "";
let timestamp = JSPLib.storage.checkStorageData('tisas-recent-timestamp',ValidateProgramData,localStorage);
if (timestamp) {
record_html = `<a id="tisas-current-records" style="color:grey" title="${new Date(timestamp).toLocaleString()}">${TimeAgo(timestamp)}</a>`
}
return record_html;
}
function RenderDatabaseVersion() {
let timestring = new Date(TISAS.server_info.timestamp).toLocaleString();
return `<a id="tisas-database-version" style="color:#0073ff" target="_blank" href="${TISAS.domain}/post_versions?page=b${TISAS.server_info.post_version+1}" title="${timestring}">${TISAS.server_info.post_version}</a>`;
}
function RenderDownloadLinks($tweet,position) {
let tweet_id = $tweet.data('tweet-id');
let user_id = $tweet.data('user-id');
let user_name = $tweet.data('screen-name');
let date_string = GetDateString(Date.now());
let time_string = GetTimeString(Date.now());
let filename_prefix = TISAS.user_settings.filename_prefix_format.replace(/%TWEETID%/g,tweet_id).replace(/%USERID%/g,user_id).replace(/%USERACCOUNT%/g,user_name).replace(/%DATE%/g,date_string).replace(/%TIME%/g,time_string);
var image_links = $("[data-image-url]",$tweet).map((i,image)=>{return image.dataset.imageUrl;}).toArray();
var hrefs = image_links.map((image)=>{return image + ':orig'});
let html = `<span style="font-size:90%;text-decoration:underline">Download Originals</span><br>`;
for (let i = 0; i < image_links.length; i++) {
let [image_name,extension] = image_links[i].slice(image_links[i].lastIndexOf('/')+1).split('.');
let download_filename = filename_prefix.replace(/%ORDER%/g,'img' + (i + 1)).replace(/%IMG%/g,image_name) + '.' + extension;
html += `<span style="margin:0 0.5em;font-size:75%"><a class="tisas-download-original" href="${hrefs[i]}" download="${download_filename}">Image #${i + 1}</a></span>`;
}
if (image_links.length > 1) {
html += `<span style="margin:0 0.5em;font-size:75%"><a class="tisas-download-all" href="#">All images</a></span>`;
}
if (position === "above") {
html = '<hr>' + html;
} else if (position === "below") {
html += '<hr>';
}
return `
<div id="tisas-download-section" style="font-size:24px;line-height:30px;letter-spacing:.01em;white-space:normal">
${html}
</div>`
}
function RenderAllSimilar(all_iqdb_results,image_urls) {
var image_results = [];
all_iqdb_results.forEach((iqdb_results,i)=>{
let html = RenderSimilarContainer("Image " + (i + 1), iqdb_results, image_urls[i]);
image_results.push(html);
});
return image_results.join('\r\n<hr>\r\n');
}
function RenderSimilarContainer(header,iqdb_results,image_url) {
var html = "";
iqdb_results.forEach((iqdb_result,i)=>{
let addons = RenderSimilarAddons(iqdb_result.post.source, iqdb_result.score, iqdb_result.post.file_ext, iqdb_result.post.file_size, iqdb_result.post.image_width, iqdb_result.post.image_height);
html += RenderPostPreview(iqdb_result.post,addons)
});
let domain = 'twitter.com';
let file_type = GetFileExtension(image_url,':');
let thumb_url = GetThumbUrl(image_url,':','jpg') + ':small';
let append_html = RenderSimilarAddons('https://twitter.com', null, file_type);
return `
<div class="tisas-iqdb-result">
<h4 style="margin-top: 0;margin-bottom: 5px;font-size: 16px;font-weight: bold;">${header}</h4>
<article class="tisas-post-preview" style="display:inline-block;width:150px;text-align:center;font-family: Verdana, Helvetica, sans-serif">
<img width="150" height="150" src="${thumb_url}">
${append_html}
</article>
${html}
</div>
`
}
function RenderPostPreview(post,append_html="") {
let [width,height] = GetPreviewDimensions(post.image_width, post.image_height);
return `
<article class="tisas-post-preview" style="display:inline-block;width:150px;text-align:center;font-family: Verdana, Helvetica, sans-serif;" data-id="${post.id}">
<a target="_blank" href="https://danbooru.donmai.us/posts/${post.id}">
<img width="${width}" height="${height}" title="${GetLinkTitle(MapPost(post),true)}">
</a>
${append_html}
</article>
`;
}
function RenderSimilarAddons(source,score,file_ext,file_size,width,height) {
var title_text = (JSPLib.validate.isNumber(score) ? `Similarity: ${JSPLib.utility.setPrecision(score,2)}` : "Original image");
var domain = (source.match(/^https?:\/\//) ? GetBaseDomainName(source) : "NON-WEB");
let size_text = (Number.isInteger(file_size) && Number.isInteger(width) && Number.isInteger(height) ? `${ReadableBytes(file_size)} (${width}x${height})` : "");
return `
<p class="tisas-desc tisas-desc-title" style="font-size:12px;margin-bottom:2px;margin-top:0">${title_text}</p>
<p class="tisas-desc tisas-desc-info" style="font-size:12px;margin-bottom:2px;margin-top:0">${file_ext.toUpperCase()} @ ${domain}</p>
<p class="tisas-desc tisas-desc-size" style="font-size:12px;margin-bottom:2px;;margin-top:0;letter-spacing:-1px">${size_text}</p>
`;
}
//Initialize functions
function InitializeDatabaseLink() {
var database_html = "";
TISAS.server_info = JSPLib.storage.getStorageData('tisas-remote-database', localStorage);
if (TISAS.server_info === null) {
return;
}
let database_timestring = new Date(TISAS.server_info.timestamp).toLocaleString();
//Add some validation to the following, and move it out of the RenderMenu function
JSPLib.storage.retrieveData('tisas-database-info').then((database_info)=>{
if (!JSPLib.validate.isHash(database_info)) {
database_html = `<a style="color:#0073ff" href="#" id="tisas-install" title="${database_timestring}">Install Database</a>`;
} else if (database_info.post_version === TISAS.server_info.post_version && database_info.timestamp === TISAS.server_info.timestamp) {
TISAS.database_info = database_info;
database_html = RenderDatabaseVersion();
} else {
TISAS.upgrade_info = database_info;
database_html = `<a style="color:#0073ff" href="#" id="tisas-upgrade" title="${database_timestring}">Upgrade Database</a>`;
}
$("#tisas-database-stub").replaceWith(database_html);
$("#tisas-install").one('click.tisas',InstallDatabase);
$("#tisas-upgrade").one('click.tisas',UpgradeDatabase);
});
}
function InitializeQtip($obj,tweet_id) {
const qtip_settings = Object.assign(ARTIST_QTIP_SETTINGS, {
content: {
text: (event, qtip) => {
if (qtip.tooltip.css('max-width') !== "none") {
qtip.tooltip.css('max-width','none');
}
return CheckIQDB.tweet_qtip[tweet_id] || "Loading...";
}
}
});
$obj.qtiptisas(qtip_settings);
}
function InitializeSimilarContainer(image_urls,all_iqdb_results,tweet_id) {
let $attachment = $(RenderAllSimilar(all_iqdb_results,image_urls));
$("article:first-of-type",$attachment).each((i,article)=>{
let image = $("img",article)[0];
let image_url = image_urls[i] + ':orig';
let fake_image = $('<img style="display:none">')[0];
fake_image.onload = function () {
JSPLib.debug.debuglog("Image onload called!",image_url);
let [width,height] = GetPreviewDimensions(fake_image.naturalWidth,fake_image.naturalHeight);
image.width = width;
image.height = height;
let starttime = Date.now();
GetImageSize(image_url).then((size)=>{
$("p:nth-child(4)",article).html(`${ReadableBytes(size)} (${fake_image.naturalWidth}x${fake_image.naturalHeight})`);
});
};
fake_image.src = image_url;
});
Promise.all(CheckIQDB.thumb_wait[tweet_id]).then((data)=>{
data.forEach((item)=>{
let $image = $(`[data-id=${item.post_id}] img`,$attachment);
$(`[data-id=${item.post_id}] img`,$attachment).attr('src',item.blob_url);
});
});
return $attachment;
}
//Network functions
async function CheckPostvers() {
if (!TISAS.database_info || !JSPLib.concurrency.reserveSemaphore('tisas')) {
return;
}
JSPLib.debug.debugTime("CheckPostvers");
let postver_lastid = GetPostVersionsLastID();
let url_addons = {search:{id:`${postver_lastid}..${postver_lastid + query_batch_size}`}};
let post_versions = await JSPLib.danbooru.getAllItems('post_versions', query_limit, {page:postver_lastid, addons: url_addons, reverse: true, domain: TISAS.domain});
if (post_versions.length === query_batch_size) {
JSPLib.debug.debuglog("Overflow detected!");
JSPLib.storage.setStorageData('tisas-overflow',true,localStorage);
} else {
JSPLib.debug.debuglog("No overflow:",post_versions.length,query_batch_size);
JSPLib.storage.setStorageData('tisas-overflow',false,localStorage);
}
let [add_entries,rem_entries] = ProcessPostvers(post_versions);
JSPLib.debug.debuglog("Process:",add_entries,rem_entries);
let combined_keys = JSPLib.utility.setIntersection(Object.keys(add_entries),Object.keys(rem_entries));
combined_keys.forEach((tweet_id)=>{
let tweet_key = 'tweet-' + tweet_id;
let post_ids = add_entries[tweet_id];
JSPLib.storage.retrieveData(tweet_key).then((data)=>{
if (JSPLib.validate.validateIDList(data)) {
JSPLib.debug.debuglog("Tweet adds/rems - existing IDs:",tweet_key,data);
post_ids = JSPLib.utility.setDifference(JSPLib.utility.setUnion(data,add_entries[tweet_id]),rem_entries[tweet_id]);
}
if (data === null || JSPLib.utility.setSymmetricDifference(post_ids,data)) {
JSPLib.debug.debuglog("Tweet adds/rems - saving:",tweet_key,post_ids);
JSPLib.storage.saveData(tweet_key,post_ids);
}
});
});
let single_adds = JSPLib.utility.setDifference(Object.keys(add_entries),combined_keys);
single_adds.forEach((tweet_id)=>{
let tweet_key = 'tweet-' + tweet_id;
let post_ids = add_entries[tweet_id];
JSPLib.storage.retrieveData(tweet_key).then((data)=>{
if (JSPLib.validate.validateIDList(data)) {
JSPLib.debug.debuglog("Tweet adds - existing IDs:",tweet_key,data);
post_ids = JSPLib.utility.setUnion(data,post_ids);
}
if (data === null || post_ids.length > data.length) {
JSPLib.debug.debuglog("Tweet adds - saving:",tweet_key,post_ids);
JSPLib.storage.saveData(tweet_key,post_ids);
}
});
});
let single_rems = JSPLib.utility.setDifference(Object.keys(rem_entries),combined_keys);
single_rems.forEach((tweet_id)=>{
let tweet_key = 'tweet-' + tweet_id;
let post_ids = [];
JSPLib.storage.retrieveData(tweet_key).then((data)=>{
if (data !== null && JSPLib.validate.validateIDList(data)) {
JSPLib.debug.debuglog("Tweet removes - existing IDs:",tweet_key,data);
post_ids = JSPLib.utility.setDifference(data,rem_entries[tweet_id]);
}
if (post_ids.length) {
JSPLib.debug.debuglog("Tweet removes - saving:",tweet_key,post_ids);
JSPLib.storage.saveData(tweet_key,post_ids);
} else {
JSPLib.debug.debuglog("Tweet removes - deleting:",tweet_key);
JSPLib.storage.removeData(tweet_key);
}
});
});
let lastid = JSPLib.danbooru.getNextPageID(post_versions,true);
//Since the post version last ID is critical, an extra sanity check has been added
if (JSPLib.validate.validateID(lastid)) {
JSPLib.storage.setStorageData('tisas-postver-lastid', lastid, localStorage);
let all_timestamps = JSPLib.utility.getObjectAttributes(post_versions,'updated_at');
let normal_timestamps = all_timestamps.map((timestamp)=>{return new Date(timestamp).getTime();})
let most_recent_timestamp = Math.max(...normal_timestamps);
JSPLib.storage.setStorageData('tisas-recent-timestamp', most_recent_timestamp, localStorage);
$("#tisas-current-records").replaceWith(RenderCurrentRecords());
}
JSPLib.concurrency.setRecheckTimeout('tisas-timeout',GetPostVersionsExpiration());
JSPLib.concurrency.freeSemaphore('tisas');
JSPLib.debug.debugTimeEnd("CheckPostvers");
}
function GetImage(image_url) {
return new Promise((resolve,reject)=>{
GM.xmlHttpRequest({
method: "GET",
url: image_url,
responseType: 'blob',
onload: function(resp) {resolve(resp.response);},
onerror: function(resp) {reject(resp);}
});
});
}
function GetImageSize(image_url) {
return new Promise((resolve,reject)=>{
GM.xmlHttpRequest({
method: "HEAD",
url: image_url,
onload: function(resp) {
let size = -1;
let match = resp.responseHeaders.match(/content-length: (\d+)/);
if (match) {
size = parseInt(match[1]);
}
resolve(size);
},
onerror: function(resp) {reject(resp);}
});
});
}
//Database functions
async function LoadDatabase() {
JSPLib.debug.debuglog("starting tweet load");
JSPLib.debug.debugTime("database-network");
var tweet_data = await $.getJSON(server_database_url);
JSPLib.debug.debugTimeEnd("database-network");
return SaveDatabase(tweet_data,"#tisas-counter");
}
async function SaveDatabase(database,counter_selector) {
var database_keys = Object.keys(database);
let batches = Math.floor(database_keys.length / 2000);
JSPLib.debug.debuglog("Database size:",database_keys.length);
var payload = {};
JSPLib.debug.debugTime("database-save-all");
for (var i = 0; i < database_keys.length; i++) {
let key = database_keys[i];
payload['tweet-' + key] = database[key];
if (i !== 0 && (i % 2000 === 0)) {
$(counter_selector).html(--batches);
JSPLib.debug.debuglog("Saving batch #",batches);
JSPLib.debug.debugTime("database-save-" + batches);
await JSPLib.storage.danboorustorage.setItems(payload);
JSPLib.debug.debugTimeEnd("database-save-" + batches);
//Give some control back to the user
await JSPLib.utility.sleep(500);
payload = {};
}
}
JSPLib.debug.debugTimeEnd("database-save-all");
}
async function CheckDatabaseInfo(initial) {
if (initial || JSPLib.concurrency.checkTimeout('tisas-database-recheck',JSPLib.utility.one_day)) {
let database_info = await $.getJSON(database_info_url);
JSPLib.storage.setStorageData('tisas-remote-database', database_info, localStorage);
JSPLib.concurrency.setRecheckTimeout('tisas-database-recheck',JSPLib.utility.one_day);
}
}
//Event handlers
function ToggleArtistHilights(event) {
if (TISAS.account) {
let no_highlight_list = GetArtistList();
let fade_selectors = JSPLib.utility.joinList(TISAS.user_settings.score_levels_faded,'.tisas-',',');
let hide_selectors = JSPLib.utility.joinList(TISAS.user_settings.score_levels_hidden,'.tisas-',',');
if (no_highlight_list.includes(TISAS.account)) {
no_highlight_list = JSPLib.utility.setDifference(no_highlight_list,[TISAS.account]);
$(fade_selectors).addClass("tisas-fade");
$(hide_selectors).addClass("tisas-hide");
$("#tisas-enable-highlights").hide();
$("#tisas-disable-highlights").show();
} else {
no_highlight_list = JSPLib.utility.setUnion(no_highlight_list,[TISAS.account]);
$(fade_selectors).removeClass("tisas-fade");
$(hide_selectors).removeClass("tisas-hide");
$("#tisas-enable-highlights").show();
$("#tisas-disable-highlights").hide();
}
JSPLib.storage.setStorageData('tisas-no-highlight-list',no_highlight_list,localStorage);
}
event.preventDefault();
}
function InstallDatabase(event) {
let message = `
This will install the database (${TISAS.server_info.post_version}, ${new Date(TISAS.server_info.timestamp).toLocaleString()}).
This can take a couple of minutes.
Click OK when ready.
`;
if (confirm(message.trim())) {
$("#tisas-install").replaceWith(load_counter)
LoadDatabase().then(()=>{
JSPLib.storage.saveData('tisas-database-info',{post_version: TISAS.server_info.post_version, timestamp: TISAS.server_info.timestamp});
$("#tisas-load-message").replaceWith(RenderDatabaseVersion());
Danbooru.Utility.notice("Database installed!");
});
}
event.preventDefault();
}
function UpgradeDatabase(event) {
let message = `
This will upgrade the database to (${TISAS.server_info.post_version}, ${new Date(TISAS.server_info.timestamp).toLocaleString()}).
Old database is at (${TISAS.upgrade_info.post_version}, ${new Date(TISAS.upgrade_info.timestamp).toLocaleString()}).
This can take a couple of minutes.
Click OK when ready.
`;
if (confirm(message.trim())) {
$("#tisas-upgrade").replaceWith(load_counter);
LoadDatabase().then(()=>{
JSPLib.storage.saveData('tisas-database-info',{post_version: TISAS.server_info.post_version, timestamp: TISAS.server_info.timestamp});
$("#tisas-load-message").replaceWith(RenderDatabaseVersion());
Danbooru.Utility.notice("Database upgraded!");
});
}
event.preventDefault();
}
function CheckUrl(event) {
let $link = $(event.target);
$link.removeClass('tisas-check-url').html("loading…");
let $tweet = $link.closest(".tweet");
let tweet_id = $tweet.data("tweet-id");
let screen_name = $tweet.data("screen-name");
let normal_url = `https://twitter.com/${screen_name}/status/${tweet_id}`;
let wildcard_url = `https://twitter.com/*/status/${tweet_id}`;
let check_url = (TISAS.user_settings.URL_wildcards_enabled ? wildcard_url : normal_url);
JSPLib.debug.debuglog("CheckURL",check_url);
JSPLib.danbooru.submitRequest('posts',{tags: "source:" + check_url},[],null,TISAS.domain,true).then((data)=>{
JSPLib.debug.debuglog("Data:",data);
var postlink;
if (data.length === 0) {
postlink = '<span style="color:red">no sources</span>';
if (TISAS.user_settings.autocheck_IQDB_enabled) {
$tweet.find(".tisas-check-iqdb").click();
}
} else {
let mapped_data = MapPostData(data);
mapped_data.forEach((post,i)=>{
let post_id = data[i].id;
JSPLib.storage.saveData('post-' + post_id, {value: post, expires: JSPLib.utility.getExpiration(JSPLib.utility.one_day)});
});
let post_ids = JSPLib.utility.getObjectAttributes(data,'id');
if (data.length === 1) {
postlink = `<a style="color:green;font-weight:bold" target="_blank" href="${TISAS.domain}/posts/${post_ids[0]}" class="tisas-confirm-delete" title="${GetLinkTitle(mapped_data[0])}">post #${post_ids[0]}</a>`
} else {
postlink = `<a style="color:green;font-weight:bold" target="_blank" href="${TISAS.domain}/posts?tags=status%3Aany+id%3A${post_ids.join(',')}" class="tisas-confirm-delete">${post_ids.length} sources</a>`;
}
JSPLib.storage.saveData('tweet-' + tweet_id, post_ids);
}
$(event.target).replaceWith(postlink);
});
event.preventDefault();
}
function CheckIQDB(event) {
let $link = $(event.target);
$link.removeClass('tisas-check-url').html("loading…");
let $tweet = $link.closest(".tweet");
let tweet_id = $tweet.data("tweet-id");
let image_urls = $tweet.find("[data-image-url]").map((i,entry)=>{return $(entry).data('image-url');}).toArray();
JSPLib.debug.debuglog("CheckIQDB",image_urls);
let promise_array = image_urls.map((image_url)=>{return JSPLib.danbooru.submitRequest('iqdb_queries',{url: image_url},[],null,TISAS.domain,true);});
Promise.all(promise_array).then((data)=>{
var postlink;
var has_results = false;
let flat_data = data.flat();
let unique_posts = RemoveDuplicates(JSPLib.utility.getObjectAttributes(flat_data,'post'),'id');
if (flat_data.length === 0) {
postlink = '<span style="color:red">no sources</span>';
} else {
has_results = true;
let post_data = JSPLib.utility.getObjectAttributes(flat_data,'post');
let mapped_data = MapPostData(unique_posts);
mapped_data.forEach((post,i)=>{
let post_id = unique_posts[i].id;
JSPLib.storage.saveData('post-' + post_id, {value: post, expires: JSPLib.utility.getExpiration(JSPLib.utility.one_day)});
});
let max_score = Math.max(...JSPLib.utility.getObjectAttributes(flat_data,'score'));
let color = "red";
if (max_score > 95.0) {
color = "green";
} else if (max_score > 90.0) {
color = "blue";
} else if (max_score > 85.0) {
color = "orange";
}
let post_ids = JSPLib.utility.getObjectAttributes(flat_data,'post_id');
TISAS.IQDB_results = TISAS.IQDB_results || {};
TISAS.IQDB_results[tweet_id] = post_ids;
if (post_ids.length === 1) {
postlink = `<a style="color:${color};font-weight:bold" target="_blank" href="${TISAS.domain}/posts/${post_ids[0]}" class="tisas-confirm-iqdb" title="${GetLinkTitle(mapped_data[0])}">post #${post_ids[0]}</a>`
} else {
postlink = `<a style="color:${color};font-weight:bold" target="_blank" href="${TISAS.domain}/posts?tags=status%3Aany+id%3A${post_ids.join(',')}" class="tisas-confirm-iqdb">${post_ids.length} sources</a>`;
}
if (TISAS.user_settings.autosave_IQDB_enabled) {
JSPLib.storage.saveData('tweet-' + tweet_id, post_ids);
}
}
let $results_link = $(postlink);
$(event.target).replaceWith($results_link);
if (has_results && TISAS.user_settings.advanced_tooltips_enabled) {
InitializeQtip($results_link,tweet_id);
//Some elements are delayed in rendering, so render ahead of time
CheckIQDB.thumb_wait[tweet_id] = unique_posts.map(async (post)=>{
let blob = await GetImage(post.preview_file_url);
let image_blob = blob.slice(0, blob.size, "image/jpeg");
let blob_url = window.URL.createObjectURL(image_blob);
return {
post_id: post.id,
blob_url: blob_url
};
});
CheckIQDB.tweet_qtip[tweet_id] = InitializeSimilarContainer(image_urls,data,tweet_id);
}
});
event.preventDefault();
}
CheckIQDB.tweet_qtip = {};
CheckIQDB.IQDB_results = {};
CheckIQDB.thumb_wait = {};
function ConfirmIQDB(event) {
if (!TISAS.user_settings.confirm_IQDB_enabled || TISAS.user_settings.autosave_IQDB_enabled) {
return;
}
let $tweet = $(event.target).closest(".tweet");
let tweet_id = $tweet.data("tweet-id");
let post_ids = TISAS.IQDB_results[tweet_id];
let confirmed_ids = prompt("Which of the following post IDs are valid IQDB hits?",post_ids.join(','));
if (confirmed_ids === null) {
return;
}
confirmed_ids = confirmed_ids.split(',').map(Number).filter((num)=>{return JSPLib.validate.validateID(num);});
JSPLib.debug.debuglog("Confirmed IDs:",confirmed_ids);
if (confirmed_ids.length) {
JSPLib.storage.saveData('tweet-' + tweet_id, confirmed_ids);
}
$(event.target).removeClass("tisas-confirm-iqdb");
event.preventDefault();
}
function ConfirmDelete(event) {
if (!TISAS.user_settings.confirm_delete_enabled) {
return;
}
let $tweet = $(event.target).closest(".tweet");
let tweet_id = $tweet.data("tweet-id");
if (confirm("Delete this tweet info?")) {
JSPLib.storage.removeData('tweet-' + tweet_id);
}
$(event.target).removeClass("tisas-confirm-delete");
event.preventDefault();
}
function DownloadOriginal(event) {
let $link = $(event.target);
let image_link = $link.attr('href');
let download_name = $link.attr('download');
JSPLib.debug.debuglog("Saving",image_link,"as",download_name);
saveAs(image_link,download_name);
event.preventDefault();
}
function DownloadAll(event) {
let $image_links = $(event.target).closest('.tweet').find('.tisas-download-original');
$image_links.click();
event.preventDefault();
}
function ExportData(event) {
if (!ExportData.is_running) {
ExportData.is_running = true;
Danbooru.Utility.notice("Exporting data!");
let save_package = {program_data: {}, tweet_database: {}};
Object.keys(localStorage).forEach((key)=>{
if (key.match(/^tisas-/)) {
save_package.program_data[key] = JSPLib.storage.getStorageData(key,localStorage);
}
});
JSPLib.storage.danboorustorage.length().then((length)=>{
let batch_length = batch_counter = Math.floor(length / 10000);
JSPLib.debug.debugTime('package-data');
return JSPLib.storage.danboorustorage.iterate((value,key,i)=>{
if (key.match(/^tweet-\d+/)) {
save_package.tweet_database[key] = value;
} else if (key === 'tisas-database-info') {
save_package.database_info = value;
}
if ((i % 10000) === 0) {
$("#tisas-export-counter").html(--batch_counter);
}
});
}).then(()=>{
JSPLib.debug.debugTimeEnd('package-data');
ExportData.is_running = false;
let filename = "TISAS-data-" + GetNumericTimestamp(Date.now()) + ".json";
DownloadObject(save_package, filename, true);
});
}
event.preventDefault();
}
ExportData.is_running = false;
function ImportData(event) {
if (!ImportData.is_running) {
ImportData.is_running = true;
JSPLib.debug.debugTime('import-data');
ReadFileAsync("#tisas-import-file",true).then((import_package)=>{
JSPLib.debug.debugTimeEnd('import-data');
Danbooru.Utility.notice("Importing data!");
let promise_array = [];
JSPLib.debug.debuglog("Program data:",import_package.program_data);
Object.keys(import_package.program_data).forEach((key)=>{
JSPLib.storage.setStorageData(key,import_package.program_data[key],localStorage);
});
JSPLib.debug.debuglog("Database info:",import_package.database_info);
promise_array.push(JSPLib.storage.saveData('tisas-database-info',import_package.database_info));
JSPLib.debug.debuglog("Database length:",Object.keys(import_package.tweet_database).length);
promise_array.push(SaveDatabase(import_package.tweet_database,"#tisas-import-counter"));
Promise.all(promise_array).then(()=>{
Danbooru.Utility.notice("Database imported! Refreshing page...");
//It's easier to just reload the page instead of re-rendering everything
setTimeout(()=>{window.location = window.location;},JSPLib.utility.one_second * 5);
ImportData.is_running = false;
});
}).catch((error)=>{
JSPLib.debug.debugTimeEnd('import-data');
ImportData.is_running = false;
});
}
event.preventDefault();
}
ExportData.is_running = false;
//Main execution functions
function RegularCheck() {
if ($(".tweet").count === 0) {
clearInterval(RegularCheck.timer);
JSPLib.debug.debuglog("Bye!",window.location.href);
return;
}
let orig_account = TISAS.account;
let $tweets = $(".tweet:not([tisas])");
let [pagetype,pageid] = GetPageType();
if (pagetype === null) {
return;
}
//Detect if the URL has changed at all
if (TISAS.page !== pagetype || TISAS.addon !== pageid) {
let params;
TISAS.page = pagetype;
TISAS.addon = pageid;
switch(TISAS.page) {
case "home":
JSPLib.debug.debuglog("Home timeline");
TISAS.account = undefined;
break;
case "main":
case "likes":
case "replies":
JSPLib.debug.debuglog("Main timeline:",TISAS.addon);
TISAS.account = TISAS.addon;
if (TISAS.account === "following" || TISAS.account === "lists") {
return;
}
break;
case "media":
JSPLib.debug.debuglog("Media timeline:",TISAS.addon);
TISAS.account = TISAS.addon;
break;
case "list":
JSPLib.debug.debuglog("List timeline:",TISAS.addon);
TISAS.account = undefined;
break;
case "hashtag":
JSPLib.debug.debuglog("Hashtag timeline:",TISAS.addon);
TISAS.account = undefined;
break;
case "search":
JSPLib.debug.debuglog("Search timeline:",TISAS.addon);
params = JSPLib.utility.parseParams(TISAS.addon);
TISAS.queries = ParseQueries(params.q);
TISAS.account = ('from' in TISAS.queries ? TISAS.queries.from : undefined);
break;
case "tweet":
JSPLib.debug.debuglog("Tweet ID:",TISAS.addon);
TISAS.account = undefined;
break;
default:
//Do nothing
}
//Only render pages with attachment points
if (["home","main","likes","replies","media","list","search","hashtag"].includes(TISAS.page)) {
if ($("#tisas-side-menu").length === 0) {
if (TISAS.page === "search" || TISAS.page === "hashtag") {
$(".SidebarFilterModule").after(RenderMenu());
} else if (TISAS.page === "list") {
$(".dashboard-left").append(RenderMenu());
} else if (TISAS.page === "home") {
$(".DashboardProfileCard").after(RenderMenu());
} else {
$(".ProfileSidebar--withLeftAlignment").append(RenderMenu());
}
InitializeDatabaseLink();
}
UpdateArtistHilights();
//Bind events for creation/rebind
if (!JSPLib.utility.isNamespaceBound("#tisas-settings",'click','tisas')) {
$("#tisas-enable-highlights,#tisas-disable-highlights").on('click.tisas',ToggleArtistHilights);
$("#tisas-settings").on('click.tisas',OpenSettingsMenu);
//These will only get bound here on a rebind
$("#tisas-install").one('click.tisas',InstallDatabase);
$("#tisas-upgrade").one('click.tisas',UpgradeDatabase);
}
}
SetCheckPostVers();
}
//Finish processing if no new tweets
if ($tweets.length === 0) {
return;
}
JSPLib.debug.debuglog("Unprocessed:",$tweets.length);
let timeline_style = "";
if($("body").hasClass("logged-in")) {
timeline_style = "margin-left:-1em";
} else {
timeline_style = "margin-left:0.5em";
}
if (["home","main","likes","replies","list","hashtag"].includes(TISAS.page)) {
ProcessTweets($tweets,(i,entry)=>{return $(entry).find(".AdaptiveMedia:not(.is-video)").length;},".ProfileTweet-actionList",`<div class="ProfileTweet-action" style="font-size:14px;letter-spacing:-1px;font-weight:bold;${timeline_style}"></div>`);
$tweets.find(".ProfileTweet-action").css('min-width','60px');
} else if (TISAS.page === "search") {
ProcessTweets($tweets,"[data-has-cards]:not([data-card2-type])",".ProfileTweet-actionList",`<div class="ProfileTweet-action" style="font-size:14px;letter-spacing:-1px;font-weight:bold;${timeline_style}"></div>`);
if (TISAS.account && TISAS.queries.filter === "images") {
HighlightTweets();
}
$tweets.find(".ProfileTweet-action").css('min-width','60px');
} else if (TISAS.page === "media") {
ProcessTweets($tweets,null,".ProfileTweet-actionList",`<div class="ProfileTweet-action" style="font-size:14px;letter-spacing:-1px;font-weight:bold;${timeline_style}"></div>`);
HighlightTweets();
$tweets.find(".ProfileTweet-action").css('min-width','60px');
} else if (TISAS.page === "tweet") {
ProcessTweets($tweets,`[data-tweet-id=${TISAS.addon}][data-has-cards]:not([data-card2-type])`,".client-and-actions",'<span class="tisas-post-link" style="font-weight:bold;margin-left:1em"></span>');
if (TISAS.user_settings.original_download_enabled) {
let $tweet = $tweets.filter(`[data-tweet-id=${TISAS.addon}][data-has-cards]:not([data-card2-type])`);
let download_html = RenderDownloadLinks($tweet,TISAS.user_settings.download_position[0]);
if (TISAS.user_settings.download_position[0] === "above") {
$(".tweet-text",$tweet).append(download_html);
} else if (TISAS.user_settings.download_position[0] === "below") {
$(".AdaptiveMediaOuterContainer",$tweet).after(download_html);
}
}
}
if (TISAS.user_settings.auto_unhide_tweets_enabled) {
UnhideTweets();
}
if (TISAS.user_settings.display_retweet_id) {
let $retweets = $tweets.filter("[data-retweet-id]");
$retweets.each((i,$tweet)=>{
let retweet_id = $($tweet).data('retweet-id');
$(".tweet-context",$tweet).append(`<span>${retweet_id}</span>`);
});
}
$tweets.attr("tisas", "working");
}
function HighlightTweets() {
var $tweets = $(".stream-item:not(.tisas-highlight) > .tweet");
var screen_name = $tweets.data("screen-name");
let no_highlight_list = GetArtistList();
$tweets.each((i,entry)=>{
var $entry = $(entry);
$entry.parent().addClass('tisas-highlight');
var tweetid = $entry.data("tweet-id");
var replies = $(".ProfileTweet-action--reply .ProfileTweet-actionCount",entry).data("tweet-stat-count");
var retweets = $(".ProfileTweet-action--retweet .ProfileTweet-actionCount",entry).data("tweet-stat-count");
var favorites = $(".ProfileTweet-action--favorite .ProfileTweet-actionCount",entry).data("tweet-stat-count");
HighlightTweets.tweetarray.push({
id: tweetid,
replies: replies,
retweets: retweets,
favorites: favorites,
entry: entry
});
TISAS.tweet_pos.push(tweetid);
TISAS.tweet_faves.push(favorites);
});
JSPLib.debug.debuglog("Tweets:",TISAS.tweet_pos);
JSPLib.debug.debuglog("Faves:",TISAS.tweet_faves);
JSPLib.debug.debuglog("Finish:",TISAS.tweet_finish);
var current_count = $.extend({},...score_levels.map((level)=>{return {[level]: 0}}));
HighlightTweets.tweetarray.forEach((tweet)=>{
let quartile = GetTweetQuartile(tweet.id);
let level = score_levels[quartile];
current_count[level]++;
if (tweet.id in TISAS.tweet_finish) {
return;
}
var $container = $(tweet.entry).parent();
$container.removeClass(JSPLib.utility.joinList(score_levels,'tisas-',' ')).addClass(`tisas-${level}`);
if(!(no_highlight_list.includes(screen_name))) {
if (TISAS.user_settings.score_levels_faded.includes(level)) {
$container.addClass('tisas-fade');
}
if (TISAS.user_settings.score_levels_hidden.includes(level)) {
$container.addClass('tisas-hide');
}
}
});
JSPLib.debug.debuglog("Excellent:",current_count.excellent,"Good:",current_count.good,"Above average:",current_count.aboveavg,"Fair:",current_count.fair,"Belowavg:",current_count.belowavg,"Poor:",current_count.poor);
}
HighlightTweets.tweetarray = [];
function UnhideTweets() {
let $hidden_tweets = $(".Tombstone-action.js-display-this-media.btn-link:not(.clicked)");
if ($hidden_tweets.length) {
JSPLib.debug.debuglog("Found hidden tweets:", $hidden_tweets.length);
$hidden_tweets.click();
$hidden_tweets.addClass("clicked");
}
}
//Settings functions
////Need a Broadcast function here
function OpenSettingsMenu(event) {
if ($("#twitter-image-searches-and-stuff").length === 0) {
RenderSettingsMenu();
let $close = $( "#twitter-image-searches-and-stuff" ).closest(".ui-dialog").find(".ui-dialog-titlebar-close");
$close.attr('title',"Close (Shortcut: Alt+C)");
}
$( "#twitter-image-searches-and-stuff" ).dialog("open");
FixDialogPosition();
}
function CloseSettingsMenu(event) {
let $close_link = $("#twitter-image-searches-and-stuff").closest('.ui-dialog').find(".ui-dialog-titlebar-close");
$close_link.click();
}
function FixDialogPosition() {
let $container = $( "#twitter-image-searches-and-stuff" ).closest(".ui-dialog");
let match = $(".ui-dialog").css('top').match(/(.+)px/);
if (!match || parseFloat(match[1]) < 50) {
$container.css('top',"75px");
}
}
function SetQueryDomain() {
TISAS.domain = 'https://' + TISAS.user_settings.query_subdomain + '.donmai.us';
}
//Only render the settings menu on demand
function RenderSettingsMenu() {
//Create the dialog
$("body").append(`<div id="twitter-image-searches-and-stuff" title="TISAS Settings"></div>`);
$( "#twitter-image-searches-and-stuff" ).dialog({ autoOpen: false , width: 1000 });
$( "#twitter-image-searches-and-stuff" ).closest(".ui-dialog").find(".ui-dialog-titlebar-close").css('width','4em');
//Standard menu creation
$("#twitter-image-searches-and-stuff").append(tisas_menu);
$("#tisas-display-settings").append(JSPLib.menu.renderCheckbox('tisas','advanced_tooltips_enabled'));
$("#tisas-display-settings").append(JSPLib.menu.renderCheckbox('tisas','auto_unhide_tweets_enabled'));
$("#tisas-display-settings").append(JSPLib.menu.renderCheckbox('tisas','display_retweet_id'));
$("#tisas-highlight-settings").append(JSPLib.menu.renderCheckbox('tisas','score_highlights_enabled'));
$("#tisas-highlight-settings").append(JSPLib.menu.renderTextinput('tisas','score_window_size',5));
$("#tisas-highlight-settings").append(JSPLib.menu.renderInputSelectors('tisas','score_levels_faded','checkbox'));
$("#tisas-highlight-settings").append(JSPLib.menu.renderInputSelectors('tisas','score_levels_hidden','checkbox'));
$("#tisas-database-settings").append(JSPLib.menu.renderCheckbox('tisas','confirm_delete_enabled'));
$("#tisas-database-settings").append(JSPLib.menu.renderCheckbox('tisas','confirm_IQDB_enabled'));
$("#tisas-database-settings").append(JSPLib.menu.renderCheckbox('tisas','autosave_IQDB_enabled'));
$("#tisas-database-settings").append(JSPLib.menu.renderCheckbox('tisas','autocheck_IQDB_enabled'));
$("#tisas-network-settings").append(JSPLib.menu.renderCheckbox('tisas','URL_wildcards_enabled'));
$("#tisas-network-settings").append(JSPLib.menu.renderTextinput('tisas','recheck_interval',5));
$("#tisas-network-settings").append(JSPLib.menu.renderInputSelectors('tisas','query_subdomain','radio'));
$("#tisas-download-settings").append(JSPLib.menu.renderCheckbox('tisas','original_download_enabled'));
$("#tisas-download-settings").append(JSPLib.menu.renderInputSelectors('tisas','download_position','radio'));
$("#tisas-download-settings").append(JSPLib.menu.renderTextinput('tisas','filename_prefix_format',80));
$("#tisas-cache-settings").append(`<div class="jsplib-menu-item"><h4>Import file</h4><input size="50" type="file" name="tisas-import-file" id="tisas-import-file"></div>`);
$("#tisas-cache-settings").append(JSPLib.menu.renderLinkclick("tisas",'import_data',`Import data (<span id="tisas-import-counter">...</span>)`,"Click to import","Imports a JSON file containing cache and program data."));
$("#tisas-cache-settings").append(JSPLib.menu.renderLinkclick("tisas",'export_data',`Export data (<span id="tisas-export-counter">...</span>)`,"Click to export","Exports cache and program data to a JSON file."));
//$("#tisas-cache-settings").append(JSPLib.menu.renderLinkclick("tisas",'cache_info',"Cache info","Click to populate","Calculates the cache usage of the program and compares it to the total usage."));
//$("#tisas-cache-settings").append(`<div id="tisas-cache-info-table" style="display:none"></div>`);
//$("#tisas-cache-settings").append(JSPLib.menu.renderLinkclick("tisas",'purge_cache',`Purge cache (<span id="tisas-purge-counter">...</span>)`,"Click to purge","Dumps all TISAS data with expirations. This does not include tweets."));
JSPLib.menu.engageUI('tisas',true);
JSPLib.menu.saveUserSettingsClick('tisas','TISAS');
JSPLib.menu.resetUserSettingsClick('tisas','TISAS',localstorage_keys,program_reset_keys);
$("#tisas-control-import-data").on('click.tisas',ImportData);
$("#tisas-control-export-data").on('click.tisas',ExportData);
//JSPLib.menu.cacheInfoClick('tisas',program_cache_regex,"#tisas-cache-info-table");
//JSPLib.menu.purgeCacheClick('tisas','TISAS',prune_cache_regex,"#tisas-purge-counter");
//Add CSS stylings
JSPLib.utility.setCSSStyle(menu_css,'menu');
const jquery_ui_css = GM_getResourceText("jquery_ui_css");
JSPLib.utility.setCSSStyle(jquery_ui_css,'jquery');
}
//Main function
function Main() {
Danbooru.TISAS = TISAS = {
tweet_pos: [],
tweet_faves: [],
tweet_finish: {},
settings_config: settings_config
};
TISAS.user_settings = JSPLib.menu.loadUserSettings('tisas');
SetQueryDomain();
RegularCheck.timer = setInterval(RegularCheck,program_recheck_interval);
$(document).on("click.tisas",".tisas-check-url",CheckUrl);
$(document).on("click.tisas",".tisas-check-iqdb",CheckIQDB);
$(document).on("click.tisas",".tisas-confirm-iqdb",ConfirmIQDB);
$(document).on("click.tisas",".tisas-confirm-delete",ConfirmDelete);
$(document).on("click.tisas",".tisas-download-original",DownloadOriginal);
$(document).on("click.tisas",".tisas-download-all",DownloadAll);
$(document).on("keydown.tisas", null, 'alt+h', ToggleArtistHilights);
$(document).on("keydown.tisas", null, 'alt+m', OpenSettingsMenu);
$(document).on("keydown.tisas", null, 'alt+c', CloseSettingsMenu);
$("body").append(notice_banner);
$("#close-notice-link").on("click.tisas",Danbooru.Utility.closeNotice);
JSPLib.utility.setCSSStyle(program_css,'program');
JSPLib.utility.setCSSStyle(GM_getResourceText('jquery_qtip_css'),'qtip');
if (JSPLib.storage.getStorageData('tisas-remote-database', localStorage) === null) {
CheckDatabaseInfo(true).then(()=>{
Danbooru.Utility.notice("TISAS will momentarily refresh the page to finish initializing.");
setTimeout(()=>{window.location = window.location;},JSPLib.utility.one_second * 5);
JSPLib.concurrency.setRecheckTimeout('tisas-database-recheck',JSPLib.utility.one_day);
});
} else {
setTimeout(()=>{
CheckDatabaseInfo();
JSPLib.storage.pruneEntries('tisas',prune_cache_regex,program_expires);
},JSPLib.utility.one_minute);
}
JSPLib.debug.debuglog("Hi!",window.location.href);
}
/****Execution start****/
JSPLib.load.programInitialize(Main,'TISAS',program_load_required_variables,program_load_required_selectors);
@BrokenEagle
Copy link
Author

This script has been moved to GitHub and will no longer be supported here.
https://github.com/BrokenEagle/JavaScripts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment