Last active
October 8, 2019 01:40
-
-
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.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* 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': '×' | |
}) | |
); | |
} | |
// 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 )); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ==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); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This script has been moved to GitHub and will no longer be supported here.
https://github.com/BrokenEagle/JavaScripts