// ==UserScript==
// @name Reddit Enhancement Suite
// @namespace http://reddit.honestbleeps.com/
// @description A suite of tools to enhance reddit...
// @copyright 2010-2013, Steve Sobel (http://redditenhancementsuite.com/)
// @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
// @author honestbleeps
// @include http://redditenhancementsuite.com/*
// @include http://reddit.honestbleeps.com/*
// @include http://reddit.com/*
// @include https://reddit.com/*
// @include http://*.reddit.com/*
// @include https://*.reddit.com/*
// @version 4.3.0.4
// @updateURL http://redditenhancementsuite.com/latest/reddit_enhancement_suite.meta.js
// @downloadURL http://redditenhancementsuite.com/latest/reddit_enhancement_suite.user.js
// @require https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
// ==/UserScript==
/*jshint undef: true, unused: true, strict: false, laxbreak: true, multistr: true, smarttabs: true, sub: true, browser: true */
var RESVersion = "4.3.0.4";
var jQuery, $, guiders, Tinycon, SnuOwnd;
/*
Reddit Enhancement Suite - a suite of tools to enhance Reddit
Copyright (C) 2010-2012 - honestbleeps (steve@honestbleeps.com)
RES is released under the GPL. However, I do ask a favor (obviously I don't/can't require it, I ask out of courtesy):
Because RES auto updates and is hosted from a central server, I humbly request that if you intend to distribute your own
modified Reddit Enhancement Suite, you name it something else and make it very clear to your users that it's your own
branch and isn't related to mine.
RES is updated very frequently, and I get lots of tech support questions/requests from people on outdated versions. If
you're distributing RES via your own means, those recipients won't always be on the latest and greatest, which makes
it harder for me to debug things and understand (at least with browsers that auto-update) whether or not people are on
a current version of RES.
I can't legally hold you to any of this - I'm just asking out of courtesy.
Thanks, I appreciate your consideration. Without further ado, the all-important GPL Statement:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
var tokenizeCSS = 'ul.token-input-list-facebook { overflow: hidden; height: auto !important; height: 1%; width: 400px; border: 1px solid #96bfe8; cursor: text; font-size: 12px; font-family: Verdana; min-height: 1px; z-index: 1010; margin: 0; padding: 0; background-color: #fff; list-style-type: none; float: left; margin-right: 12px; }';
tokenizeCSS += '.optionsTable ul.token-input-list-facebook {clear: left; float: none; margin-right: 0; }';
tokenizeCSS += 'ul.token-input-list-facebook li input { border: 0; width: 100px; padding: 3px 8px; background-color: white; margin: 2px 0; -webkit-appearance: caret; }';
tokenizeCSS += 'li.token-input-token-facebook { overflow: hidden; height: auto !important; height: 15px; margin: 3px; padding: 1px 3px; background-color: #eff2f7; color: #000; cursor: default; border: 1px solid #ccd5e4; font-size: 11px; border-radius: 5px; float: left; white-space: nowrap; }';
tokenizeCSS += 'li.token-input-token-facebook p { display: inline; padding: 0; margin: 0;}';
tokenizeCSS += 'li.token-input-token-facebook span { color: #a6b3cf; margin-left: 5px; font-weight: bold; cursor: pointer;}';
tokenizeCSS += 'li.token-input-selected-token-facebook { background-color: #5670a6; border: 1px solid #3b5998; color: #fff;}';
tokenizeCSS += 'li.token-input-input-token-facebook { float: left; margin: 0; padding: 0; list-style-type: none;}';
tokenizeCSS += 'div.token-input-dropdown-facebook { position: absolute; width: 400px; background-color: #fff; overflow: hidden; border-left: 1px solid #ccc; border-right: 1px solid #ccc; border-bottom: 1px solid #ccc; cursor: default; font-size: 11px; font-family: Verdana; z-index: 100000100; }';
tokenizeCSS += 'div.token-input-dropdown-facebook p { margin: 0; padding: 5px; font-weight: bold; color: #777;}';
tokenizeCSS += 'div.token-input-dropdown-facebook ul { margin: 0; padding: 0;}';
tokenizeCSS += 'div.token-input-dropdown-facebook ul li { background-color: #fff; padding: 3px; margin: 0; list-style-type: none;}';
tokenizeCSS += 'div.token-input-dropdown-facebook ul li.token-input-dropdown-item-facebook { background-color: #fff;}';
tokenizeCSS += 'div.token-input-dropdown-facebook ul li.token-input-dropdown-item2-facebook { background-color: #fff;}';
tokenizeCSS += 'div.token-input-dropdown-facebook ul li em { font-weight: bold; font-style: normal;}';
tokenizeCSS += 'div.token-input-dropdown-facebook ul li.token-input-selected-dropdown-item-facebook { background-color: #3b5998; color: #fff;}';
var guidersCSS = '.guider { background: #FFF; border: 1px solid #666; font-family: arial; position: absolute; outline: none; z-index: 100000005 !important; padding: 4px 12px; width: 500px; z-index: 100; -moz-box-shadow: 0 0 8px #111; -webkit-box-shadow: 0 0 8px #111; box-shadow: 0 0 8px #111; border-radius: 4px;}';
guidersCSS += '.guider_buttons { height: 36px; position: relative; width: 100%; }';
guidersCSS += '.guider_content { position: relative; }';
guidersCSS += '.guider_description { margin-bottom: 10px; }';
guidersCSS += '.guider_content h1 { color: #1054AA; float: left; font-size: 21px; }';
guidersCSS += '.guider_close { float: right; padding: 10px 0 0; }';
// guidersCSS += '.x_button { background-image: url(\'x_close_button.jpg\'); cursor: pointer; height: 13px; width: 13px; }';
guidersCSS += '.x_button { background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAIAAAD9iXMrAAAACXBIWXMAAAsTAAALEwEAmpwYAAABe0lEQVQoFT2RT0/CQBDFd7tbqIULiAaMgK1ngSqFCyHwyaFANS1+BEDigRgIFORAQv/5lhI32eR1duY3b6Z0vvg+Ho+M0TAMGWPx5VBKCSH/OpPJ8OVyqaqKqqqcsSgKophKkpTUIBX6dDptNhuON13X7wq3F4RgkEQJnpCr1c9sNgOFhUHgebvpdNrpdNAiiiIwt1vPdd1er8c5D4IAvAjm8vl8LpebTCZ4SKXSu91+Mh7XajX1Jv17OMCs4PlBBCutVgvIwcBqGIZt269GvVqtwAalMUD8OiYhKGo2Tfv9w7Isw3jTNA3FOGiIHA6FLcByTIjn7dfrdaFwv1gsKuWSoigXntgAxxXGKYHx8WjUqL9gfMdxhta43+/LMpeYHITxled5h5FIquv6Exjw6rifw+Gw2+2ez2eMLOZF62w2a5pm+fEhaYRIu23O51+Kkkpyrn1lmRWLxcQrKmEG+vlZAwQaxRwXa0NUHHzE4i/7vh8TCSTEoEul0h9jNtRZlgw9UAAAAABJRU5ErkJggg==); cursor: pointer; height: 13px; width: 13px; }';
guidersCSS += '.guider_content p { clear: both; color: #333; font-size: 13px; }';
guidersCSS += '.guider_button { background: -moz-linear-gradient(top, #5CA9FF 0%, #3D79C3 100%); background: -webkit-gradient(linear, left top, left bottom, color-stop(0%, #5CA9FF), color-stop(100%, #3D79C3)); background-color: #4A95E0; border: solid 1px #4B5D7E; color: #FFF; cursor: pointer; display: inline-block; float: right; font-size: 75%; font-weight: bold; margin-left: 6px; min-width: 40px; padding: 3px 5px; text-align: center; text-decoration: none;border-radius: 2px; }';
guidersCSS += '#guider_overlay { background-color: #000; width: 100%; height: 100%; position: fixed; top: 0; left: 0; opacity: 0.5; filter: alpha(opacity=50); z-index: 1000; }';
/**
* For optimization, the arrows image is inlined in the css below.
*
* To use your own arrows image, replace this background-image with your own arrows.
* It should have four arrows, top, right, left, and down.
*/
guidersCSS += '.guider_arrow { width: 42px; height: 42px; position: absolute; display: none; background-repeat: no-repeat; z-index: 100000006 !important; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACoAAACoCAMAAAChZYy6AAABelBMVEX///8zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzN+fn4zMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzMzPe3t4zMzMzMzN2dnYzMzMzMzPV1dUzMzMzMzMzMzMzMzMzMzN8fHwzMzOEhIQzMzPa2tozMzMzMzNtbW0zMzNzc3MzMzMzMzPR0dFtbW1xcXHS0tIzMzMzMzNxcXHh4eEzMzMzMzPi4uIzMzMzMzMzMzMzMzMzMzMzMzOEhITPz8+MjIwzMzMzMzMzMzOHh4czMzMzMzPZ2dkzMzMzMzPPz88zMzPW1tYzMzMzMzPV1dVvb28zMzMzMzMzMzOBgYEzMzPY2NiBgYHh4eHd3d3Nzc3o6OikpKQzMzPe3t7R0dEzMzPZ2dna2tqHh4czMzMzMzP///+41NW+AAAAfXRSTlMAAAECBA0HCBUfDhYsYy4gPU07CyF4PFCLjnVhA3ZMYC1PuxSMOmJ0jSscivBOGbY+eepVDDZ3Hrk/vgbuKn+yNLUJKOmxs+pZU7TwMgXcEhcmHRNun+SzNw+HvnJe6Eok50vsZArrpHFSNbyJ137v7ufmkyXv6F/s6qFWWFA9DhkAAAWkSURBVHhe7djpU+LKHsfh3ARiQBE0ioIIKoSwCKiAiAguuG+Ay7jv+z4uozNq/vebDub8OImdpOq8O3W+L62nPkVZZTc24aEokiQMjWUnaBNlCEcicb+5zhAeSy/mLYwhPDebPHY2iJjWxcvrBb4jZggLwn4i0NnRZAAL4k4S7SJu1sOCtHNtDBRtYByLgepjoIDn290+PEYENoPFQAFPR90+b3PY4kcYQ+VNSpgLZ/wp2kPhqIxHFRioeq+jUf7Ny+VXVtkcRcoUs6OpW37vIF0pm3MUUMwep7bXks4Kc2YCit2Pi8ur64x1iQSK39199rCUM0SfnrMvJdoAfdjgk5uWLf0PsLAT2P3MFsusSYcuDLv6bTGuGLfS2r+sXhGetvU12pkU7SFlioE2EbbUW2tPiG/gjatVDYEC7EHQARBHfykhjv7sCSkgho4g2I2BQAHaZYijI0OhwWC3owsghv4eCmhBoH+0IWz5veAOxpwiNAPEHcUfXoB6B3zYzmCg/rXxzy8juOL0KRsZm1sWNLc8NxZhPQTFRtKz79p0fTYdYSmCmogvfhT+aMn9QnIxPkERJO0Pe91DWjTBH+f9NEmQJrPdGQz8xsuTQIfTYkZ/4VQd0xUb1MgmOmMNTB1FEHJ2BCfP2+UoytZ3dUNWHW2qRr+yjmAIkx34KwrZnu/pOIrSKIrNQrT5KwrZ1h79KMpa7Q5b6KdWVDc7L0eV2V9KOQPR2mwjZCHqlqPKrKtXGfXJUUW2TZmdlqPKbIsyOxmFqDp7ox+FrF4UssNAR91eiGpmX3FROds/XBMNQ1Sd7bO5FqryKOrjIKqZneK9YT+K4rOn1ezj7RuXgSgmu6MXhU8beBCEH9t7XCaFolrZ2O6GIFysHeQhisnauU/+6e4ymV5JeRDQyDLFbPL5/spZWYUoJpuKFzez2etKmYUoJktby5aXwwwDX9qwWQ/NbpVK1jOQeEuZlujckrGbhBSncP+NJCnKYwhSJnqCZQ3BOrM/HokYgow9vJgeMwDr7V1O78fsnCEYC7oL78va0CpCR3dwMDAkXu3GoHRZ68PQ0NeFpgEbHd221lAPgtJwsAWgPCxsQ/Bvlx4eunoU1xge9gqKqWEfgjcAgaphv2sYoJqSHjrF2EV4ClA5OFTiRS6GigsI4ClpYsvF7OduYAcgji5tWTaT/MYDACylSy/Z5ycAGjRXOsze3xmiS9bM9dXlxQ99SpjOmIozubY99ahLqZy5XEkf7PG3U0c6lKRy7OpKnvO+8dHRVxyVux465c+EOa/PHR2dxFI1np7EURmbaLPfEm6W8IwmJUiEGUtDc5PP3T4PGCgWDygoFnd0to8DBorHQHVx4hyoPj4BqoWdIg4k9oFq4lgHX1hfBqCJj5Ozc8YOeEseDniD14bxy8j4Ffcv3f/UI9HUP1ZR9OUpR6MvT7qUyp1ZS6UtlvaQGArSzGQOXyxlK03pUA9brlxns5vFeMpEfkfhJKBXK86r++dktoj+IZX3HfWkVtLJy7sn/pOzW00klqKoP3+wdiEIG7uxxnrIqilpSmW4vW3xQH4I2PpaUFZFIRr28lOCuJ3+NsgqKYqaM9zb7SOiC65TyKooRNGGIauiKGrhfNEjBFEWPq2KUjQT9rpHJQhZJZWjzb7oq0x7XTYHyqopijZAFGVbpayCQnQSKGQVFEWb3NNCzW6qWSWFqDqroBCtWY+craXVaPuMjCBrR9laWo3Oyway3ShbS6VohxyF/QpVszVUinaiqDrbJWa/KEQH1PRnTZaA6DgCGlkCE5U3Ego67GYpS2CikB2Us0Q16oQoLosoVYeiCQGzITlLyNFzHB0JBJ1SlpCiMRTFZ2NdjJglqtHACZ7+/sqix7/8MY+i+KwbPQCQ0pNisrCvRf8UPtCTYvWhcl3Q3Hv1odL48+f/AZYqoYKwlk9uAAAAAElFTkSuQmCC); } ';
guidersCSS += '.guider_arrow_right { display: block; background-position: 0 0; right: -42px; }';
guidersCSS += '.guider_arrowdown { display: block; background-position: 0 -42px; bottom: -42px; }';
guidersCSS += '.guider_arrow_up { display: block; background-position: 0 -126px; top: -42px; }';
guidersCSS += '.guider_arrow_left { display: block; background-position: 0 -84px; left: -42px;}';
guidersCSS += '.guider_content h2 { margin-top: 1em; }';
guidersCSS += '.guider_content td { vertical-align: top; }';
guidersCSS += '.guider_content td + td { padding-left: 1em; }';
guidersCSS += '.guider_content td code { white-space: nowrap; }';
// DOM utility functions
var escapeLookups = { "&": "&", '"': """, "<": "<", ">": ">" };
function escapeHTML(str) {
return (typeof str === 'undefined' || str === null) ?
null :
str.toString().replace(/[&"<>]/g, function(m) { return escapeLookups[m]; });
}
function insertAfter( referenceNode, newNode ) {
if ((typeof referenceNode === 'undefined') || (referenceNode === null)) {
console.log(arguments.callee.caller);
} else if ((typeof referenceNode.parentNode !== 'undefined') && (typeof referenceNode.nextSibling !== 'undefined')) {
if (referenceNode.parentNode === null) {
console.log(arguments.callee.caller);
} else {
referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling );
}
}
}
function createElementWithID(elementType, id, classname) {
var obj = document.createElement(elementType);
if (id !== null) {
obj.setAttribute('id', id);
}
if ((typeof classname !== 'undefined') && (classname !== '')) {
obj.setAttribute('class', classname);
}
return obj;
}
// this alias is to account for opera having different behavior...
if (typeof navigator === 'undefined') navigator = window.navigator;
//Because Safari 5.1 doesn't have Function.bind
if (typeof Function.prototype.bind === 'undefined') {
Function.prototype.bind = function(context) {
var oldRef = this;
return function() {
return oldRef.apply(context || null, Array.prototype.slice.call(arguments));
};
};
}
var BrowserDetect = {
init: function () {
this.browser = this.searchString(this.dataBrowser) || "An unknown browser";
this.version = this.searchVersion(navigator.userAgent) ||
this.searchVersion(navigator.appVersion) ||
"an unknown version";
this.OS = this.searchString(this.dataOS) || "an unknown OS";
// set up MutationObserver variable to take whichever is supported / existing...
// unfortunately, this doesn't (currently) exist in Opera.
// this.MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver || null;
// At the time of writing WebKit's mutation observer leaks entire pages on refresh so it needs to be disabled.
this.MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver || null;
// null out MutationObserver to test legacy DOMNodeInserted
// this.MutationObserver = null;
},
searchString: function (data) {
for (var i=0;i' + data + ' RES can delete this data to stop errors from happening, but you might want to copy/paste it to a text file so you can more easily re-enter any lost information.';
alert(msg, function() {
// back up a copy of the corrupt data
localStorage.setItem(localStorageSource + '.error', data);
// delete the corrupt data
RESStorage.removeItem(localStorageSource);
});
} else {
alert('Error caught: JSON parse failure on the following data: ' + data);
}
return {};
}
}
};
// array compare utility function for keyCode arrays
function keyArrayCompare(fromArr, toArr) {
// if we've passed in a number, fix that and make it an array with alt, shift and ctrl set to false.
if (typeof toArr === 'number') {
toArr = [toArr, false, false, false];
} else if (toArr.length === 4) {
toArr.push(false);
}
if (fromArr.length !== toArr.length) return false;
for (var i = 0; i < toArr.length; i++) {
if (fromArr[i].compare) {
if (!fromArr[i].compare(toArr[i])) return false;
}
if (fromArr[i] !== toArr[i]) return false;
}
return true;
}
// utility function for checking events against keyCode arrays
function checkKeysForEvent(event, keyArray) {
//[keycode, alt, ctrl, shift, meta]
// if we've passed in a number, fix that and make it an array with alt, shift and ctrl set to false.
if (typeof keyArray === 'number') {
keyArray = [keyArray, false, false, false, false];
} else if (keyArray.length === 4) {
keyArray.push(false);
}
if (event.keyCode != keyArray[0]) return false;
else if (event.altKey != keyArray[1]) return false;
else if (event.ctrlKey != keyArray[2]) return false;
else if (event.shiftKey != keyArray[3]) return false;
else if (event.metaKey != keyArray[4]) return false;
else return true;
}
function operaUpdateCallback(obj) {
RESUtils.compareVersion(obj);
}
function operaForcedUpdateCallback(obj) {
RESUtils.compareVersion(obj, true);
}
// This object will store xmlHTTPRequest callbacks for Safari because Safari's extension architecture seems stupid.
// This really shouldn't be necessary, but I can't seem to hold on to an onload function that I pass to the background page...
xhrQueue = { count: 0, onloads: [] };
// if this is a jetpack addon, add an event listener like Safari's message handler...
if (BrowserDetect.isFirefox()) {
self.on('message', function(msgEvent) {
switch (msgEvent.name) {
case 'GM_xmlhttpRequest':
// Fire the appropriate onload function for this xmlhttprequest.
xhrQueue.onloads[msgEvent.XHRID](msgEvent.response);
break;
case 'compareVersion':
var forceUpdate = false;
if (typeof msgEvent.message.forceUpdate !== 'undefined') forceUpdate = true;
RESUtils.compareVersion(msgEvent.message, forceUpdate);
break;
case 'loadTweet':
var tweet = msgEvent.response;
var thisExpando = modules['styleTweaks'].tweetExpando;
$(thisExpando).html(tweet.html);
thisExpando.style.display = 'block';
thisExpando.classList.add('twitterLoaded');
break;
// for now, commenting out the old way of handling tweets as AMO will not approve.
/*
var tweet = msgEvent.response;
var thisExpando = modules['styleTweaks'].tweetExpando;
thisExpando.innerHTML = '';
// the iframe is to sandbox this remote javascript from accessing reddit's javascript, etc.
// this is done this way as requested by the AMO review team.
var sandboxFrame = document.createElement('iframe');
var seamless = document.createAttribute('seamless');
sandboxFrame.setAttribute('sandbox','allow-scripts allow-same-origin');
sandboxFrame.setAttributeNode(seamless);
sandboxFrame.setAttribute('style','border: none;');
sandboxFrame.setAttribute('width','480');
sandboxFrame.setAttribute('height','260');
sandboxFrame.setAttribute('src','data:text/html,'+encodeURIComponent(tweet.html)+"");
$(thisExpando).append(sandboxFrame);
// $(thisExpando).html(tweet.html);
thisExpando.style.display = 'block';
thisExpando.classList.add('twitterLoaded');
*/
case 'getLocalStorage':
// Does RESStorage have actual data in it? If it doesn't, they're a legacy user, we need to copy
// old school localStorage from the foreground page to the background page to keep their settings...
if (typeof msgEvent.message.importedFromForeground === 'undefined') {
// it doesn't exist.. copy it over...
var thisJSON = {
requestType: 'saveLocalStorage',
data: localStorage
};
self.postMessage(thisJSON);
} else {
setUpRESStorage(msgEvent.message);
//RESInit();
}
break;
case 'saveLocalStorage':
// Okay, we just copied localStorage from foreground to background, let's set it up...
setUpRESStorage(msgEvent.message);
break;
case 'localStorage':
RESStorage.setItem(msgEvent.itemName, msgEvent.itemValue, true);
break;
default:
// console.log('unknown event type in self.on');
// console.log(msgEvent.toSource());
break;
}
});
}
// This is the message handler for Safari - the background page calls this function with return data...
function safariMessageHandler(msgEvent) {
switch (msgEvent.name) {
case 'GM_xmlhttpRequest':
// Fire the appropriate onload function for this xmlhttprequest.
xhrQueue.onloads[msgEvent.message.XHRID](msgEvent.message);
break;
case 'compareVersion':
var forceUpdate = false;
if (typeof msgEvent.message.forceUpdate !== 'undefined') forceUpdate = true;
RESUtils.compareVersion(msgEvent.message, forceUpdate);
break;
case 'loadTweet':
var tweet = msgEvent.message;
var thisExpando = modules['styleTweaks'].tweetExpando;
$(thisExpando).html(tweet.html);
thisExpando.style.display = 'block';
thisExpando.classList.add('twitterLoaded');
break;
case 'getLocalStorage':
// Does RESStorage have actual data in it? If it doesn't, they're a legacy user, we need to copy
// old schol localStorage from the foreground page to the background page to keep their settings...
if (typeof msgEvent.message.importedFromForeground === 'undefined') {
// it doesn't exist.. copy it over...
var thisJSON = {
requestType: 'saveLocalStorage',
data: localStorage
};
safari.self.tab.dispatchMessage('saveLocalStorage', thisJSON);
} else {
setUpRESStorage(msgEvent.message);
//RESInit();
}
break;
case 'saveLocalStorage':
// Okay, we just copied localStorage from foreground to background, let's set it up...
setUpRESStorage(msgEvent.message);
//RESInit();
break;
case 'addURLToHistory':
var url = msgEvent.message.url;
modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
break;
case 'localStorage':
RESStorage.setItem(msgEvent.message.itemName, msgEvent.message.itemValue, true);
break;
default:
// console.log('unknown event type in safariMessageHandler');
break;
}
}
// This is the message handler for Opera - the background page calls this function with return data...
function operaMessageHandler(msgEvent) {
var eventData = msgEvent.data;
switch (eventData.msgType) {
case 'GM_xmlhttpRequest':
// Fire the appropriate onload function for this xmlhttprequest.
xhrQueue.onloads[eventData.XHRID](eventData.data);
break;
case 'compareVersion':
var forceUpdate = false;
if (typeof eventData.data.forceUpdate !== 'undefined') forceUpdate = true;
RESUtils.compareVersion(eventData.data, forceUpdate);
break;
case 'loadTweet':
var tweet = eventData.data;
var thisExpando = modules['styleTweaks'].tweetExpando;
$(thisExpando).html(tweet.html);
thisExpando.style.display = 'block';
thisExpando.classList.add('twitterLoaded');
break;
case 'getLocalStorage':
// Does RESStorage have actual data in it? If it doesn't, they're a legacy user, we need to copy
// old schol localStorage from the foreground page to the background page to keep their settings...
if (typeof eventData.data.importedFromForeground === 'undefined') {
// it doesn't exist.. copy it over...
var thisJSON = {
requestType: 'saveLocalStorage',
data: localStorage
};
opera.extension.postMessage(JSON.stringify(thisJSON));
} else {
if (location.hostname.match('reddit')) {
setUpRESStorage(eventData.data);
//RESInit();
}
}
break;
case 'saveLocalStorage':
// Okay, we just copied localStorage from foreground to background, let's set it up...
setUpRESStorage(eventData.data);
if (location.hostname.match('reddit')) {
//RESInit();
}
break;
case 'localStorage':
if ((typeof RESStorage !== 'undefined') && (typeof RESStorage.setItem === 'function')) {
RESStorage.setItem(eventData.itemName, eventData.itemValue, true);
} else {
// a change in opera requires this wait/timeout for the RESStorage grab to work...
var waitForRESStorage = function(eData) {
if ((typeof RESStorage !== 'undefined') && (typeof RESStorage.setItem === 'function')) {
RESStorage.setItem(eData.itemName, eData.itemValue, true);
} else {
setTimeout(function() { waitForRESStorage(eData); }, 200);
}
};
var savedEventData = {
itemName: eventData.itemName,
itemValue: eventData.itemValue
};
waitForRESStorage(savedEventData);
}
break;
case 'addURLToHistory':
var url = eventData.url;
if (! eventData.isPrivate) {
modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
}
break;
default:
// console.log('unknown event type in operaMessageHandler');
break;
}
}
// listen for messages from chrome background page
if (BrowserDetect.isChrome()) {
chrome.extension.onMessage.addListener(
function(request, sender, sendResponse) {
switch(request.requestType) {
case 'localStorage':
RESStorage.setItem(request.itemName, request.itemValue, true);
break;
default:
// sendResponse({status: "unrecognized request type"});
break;
}
}
);
}
if (BrowserDetect.isSafari()) {
// Safari has a ridiculous bug that causes it to lose access to safari.self.tab if you click the back button.
// this stupid one liner fixes that.
window.onunload = function(){};
safari.self.addEventListener("message", safariMessageHandler, false);
}
// we can't do this check for opera here because we need to wait until DOMContentLoaded is triggered, I think. Putting this in RESinit();
// opera compatibility
if (BrowserDetect.isOpera()) {
// removing this line for new localStorage methodology (store in extension localstorage)
sessionStorage = window.sessionStorage;
localStorage = window.localStorage;
location = window.location;
XMLHttpRequest = window.XMLHttpRequest;
}
// Firebug stopped showing console.log for some reason. Need to use unsafeWindow if available. Not sure if this was due to a Firebug version update or what.
if (typeof unsafeWindow !== 'undefined') {
if ((typeof unsafeWindow.console !== 'undefined') && (!BrowserDetect.isFirefox())) {
console = unsafeWindow.console;
} else if (typeof console === 'undefined') {
console = {
log: function(str) {
return false;
}
};
}
}
// GreaseMonkey API compatibility for non-GM browsers (Chrome, Safari, Firefox)
// @copyright 2009, 2010 James Campos
// @modified 2010 Steve Sobel - added some missing gm_* functions
// @license cc-by-3.0; http://creativecommons.org/licenses/by/3.0/
if ((typeof GM_deleteValue === 'undefined') || (typeof GM_addStyle === 'undefined')) {
GM_addStyle = function(css) {
var style = document.createElement('style');
style.textContent = css;
var head = document.getElementsByTagName('head')[0];
if (head) {
head.appendChild(style);
}
};
GM_deleteValue = function(name) {
localStorage.removeItem(name);
};
GM_getValue = function(name, defaultValue) {
var value = localStorage.getItem(name);
if (!value)
return defaultValue;
var type = value[0];
value = value.substring(1);
switch (type) {
case 'b':
return value === 'true';
case 'n':
return Number(value);
default:
return value;
}
};
GM_log = function(message) {
console.log(message);
};
GM_registerMenuCommand = function(name, funk) {
//todo
};
GM_setValue = function(name, value) {
value = (typeof value)[0] + value;
localStorage.setItem(name, value);
};
if (BrowserDetect.browser === "Explorer") {
GM_xmlhttpRequest = function(obj) {
var request,
crossDomain = (obj.url.indexOf(location.hostname) === -1);
if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
obj.requestType = 'GM_xmlhttpRequest';
request = new XDomainRequest();
request.onload = function() {obj.onload(request);};
request.onerror = function() {if (obj.onerror) {obj.onerror(request);}};
request.open(obj.method,obj.url);
request.send(obj.data);
return request;
} else {
request = new XMLHttpRequest();
request.onreadystatechange=function() {
if (obj.onreadystatechange) {
obj.onreadystatechange(request);
}
if (request.readyState === 4 && obj.onload) {
obj.onload(request);
}
};
request.onerror = function() {
if(obj.onerror) {
obj.onerror(request);
}
};
try {
request.open(obj.method,obj.url,true);
} catch(e) {
if(obj.onerror) {
obj.onerror({
readyState:4,
responseHeaders:'',
responseText:'',
responseXML:'',
status:403,
statusText:'Forbidden'
});
}
return;
}
if(obj.headers) {
for (var name in obj.headers) {
request.setRequestHeader(name,obj.headers[name]);
}
}
request.send(obj.data);
return request;
}
};
}
if (BrowserDetect.isChrome()) {
GM_xmlhttpRequest = function(obj) {
var crossDomain = (obj.url.indexOf(location.hostname) === -1);
if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
obj.requestType = 'GM_xmlhttpRequest';
if (typeof obj.onload !== 'undefined') {
chrome.extension.sendMessage(obj, function(response) {
obj.onload(response);
});
}
} else {
var request=new XMLHttpRequest();
request.onreadystatechange = function() {
if (obj.onreadystatechange) {
obj.onreadystatechange(request);
}
if(request.readyState === 4 && obj.onload) {
obj.onload(request);
}
};
request.onerror = function() {
if(obj.onerror) {
obj.onerror(request);
}
};
try {
request.open(obj.method,obj.url,true);
} catch(e) {
if (obj.onerror) {
obj.onerror({
readyState:4,
responseHeaders:'',
responseText:'',
responseXML:'',
status:403,
statusText:'Forbidden'
});
}
return;
}
if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
request.send(obj.data); return request;
}
};
} else if (BrowserDetect.isSafari()) {
GM_xmlhttpRequest = function(obj) {
obj.requestType = 'GM_xmlhttpRequest';
// Since Safari doesn't provide legitimate callbacks, I have to store the onload function here in the main
// userscript in a queue (see xhrQueue), wait for data to come back from the background page, then call the onload.
// oy vey... another problem. When Safari sends xmlhttpRequests from the background page, it loses the cookies etc that it'd have
// had from the foreground page... so we need to write a bit of a hack here, and call different functions based on whether or
// not the request is cross domain... For same-domain requests, we'll call from the foreground...
var crossDomain = (obj.url.indexOf(location.hostname) === -1);
if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
obj.XHRID = xhrQueue.count;
xhrQueue.onloads[xhrQueue.count] = obj.onload;
safari.self.tab.dispatchMessage("GM_xmlhttpRequest", obj);
xhrQueue.count++;
} else {
var request=new XMLHttpRequest();
request.onreadystatechange = function() {
if (obj.onreadystatechange) {
obj.onreadystatechange(request);
}
if (request.readyState === 4 && obj.onload) {
obj.onload(request);
}
};
request.onerror = function() {
if(obj.onerror) {
obj.onerror(request);
}
};
try {
request.open(obj.method,obj.url,true);
} catch(e) {
if (obj.onerror) {
obj.onerror({
readyState:4,
responseHeaders:'',
responseText:'',
responseXML:'',
status:403,
statusText:'Forbidden'
});
}
return;
}
if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
request.send(obj.data); return request;
}
};
} else if (BrowserDetect.isOpera()) {
GM_xmlhttpRequest = function(obj) {
obj.requestType = 'GM_xmlhttpRequest';
// Turns out, Opera works this way too, but I'll forgive them since their extensions are so young and they're awesome people...
// oy vey... cross domain same issue with Opera.
var crossDomain = (obj.url.indexOf(location.hostname) === -1);
if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
obj.XHRID = xhrQueue.count;
xhrQueue.onloads[xhrQueue.count] = obj.onload;
opera.extension.postMessage(JSON.stringify(obj));
xhrQueue.count++;
} else {
var request=new XMLHttpRequest();
request.onreadystatechange = function() {
if (obj.onreadystatechange) {
obj.onreadystatechange(request);
}
if (request.readyState === 4 && obj.onload) {
obj.onload(request);
}
};
request.onerror = function() {
if(obj.onerror) {
obj.onerror(request);
}
};
try {
request.open(obj.method,obj.url,true);
} catch(e) {
if (obj.onerror) {
obj.onerror({
readyState:4,
responseHeaders:'',
responseText:'',
responseXML:'',
status:403,
statusText:'Forbidden'
});
}
return;
}
if (obj.headers) {
for (var name in obj.headers) {
request.setRequestHeader(name,obj.headers[name]);
}
}
request.send(obj.data); return request;
}
};
} else if (BrowserDetect.isFirefox()) {
// we must be in a Firefox / jetpack addon...
GM_xmlhttpRequest = function(obj) {
var crossDomain = (obj.url.indexOf(location.hostname) === -1);
if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
obj.requestType = 'GM_xmlhttpRequest';
// okay, firefox's jetpack addon does this same stuff... le sigh..
if (typeof obj.onload !== 'undefined') {
obj.XHRID = xhrQueue.count;
xhrQueue.onloads[xhrQueue.count] = obj.onload;
self.postMessage(obj);
xhrQueue.count++;
}
} else {
var request=new XMLHttpRequest();
request.onreadystatechange = function() {
if (obj.onreadystatechange) {
obj.onreadystatechange(request);
}
if(request.readyState === 4 && obj.onload) {
obj.onload(request);
}
};
request.onerror = function() {
if(obj.onerror) {
obj.onerror(request);
}
};
try {
request.open(obj.method,obj.url,true);
} catch(e) {
if (obj.onerror) {
obj.onerror({
readyState:4,
responseHeaders:'',
responseText:'',
responseXML:'',
status:403,
statusText:'Forbidden'
});
}
return;
}
if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
request.send(obj.data); return request;
}
};
}
} else {
// this hack is to avoid an unsafeWindow error message if a gm_xhr is ever called as a result of a jQuery-induced ajax call.
// yes, it's ugly, but it's necessary if we're using Greasemonkey together with jQuery this way.
var oldgmx = GM_xmlhttpRequest;
GM_xmlhttpRequest = function(params) {
setTimeout(function() {
oldgmx(params);
}, 0);
};
}
var modules = {};
// define common RESUtils - reddit related functions and data that may need to be accessed...
var RESUtils = {
preInit: function() {
// we store a localStorage key because the async call is too slow to add classes to
// the document prior to page load, thus the flash of unstyled content.
RESUtils.getDocHTML();
},
// to avoid the flash of unstyled content, the very first thing we should do is get a hold
// of the document object and add necessary classes...
getDocHTML: function() {
if (document) {
document.html = document.documentElement;
if (localStorage.getItem('RES_nightMode')) {
// no need to check the background - we're in night mode for sure.
modules['styleTweaks'].redditDark();
}
} else {
setTimeout(RESUtils.getDocHTML, 1);
}
},
// A cache variable to store CSS that will be applied at the end of execution...
randomHash: function(len) {
var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
var numChars = len || 5;
var randomString = '';
for (var i=0; i').html(css).appendTo('head');
return {
remove: function() { style.remove(); }
};
} else {
this.css += css;
}
},
insertParam: function(href, key, value) {
var pre = '&';
if (href.indexOf('?') === -1) pre = '?';
return href + pre + key + '=' + value;
},
// checks if script should run on current URL using exclude / include.
isMatchURL: function (moduleID) {
var i=0;
var currURL = location.href;
// get includes and excludes...
var excludes = modules[moduleID].exclude;
var includes = modules[moduleID].include;
// first check excludes...
if (typeof excludes !== 'undefined') {
for (i=0, len = excludes.length; i span.user > a');
if ((userLink !== null) && (!userLink.classList.contains('login-required'))) {
this.loggedInUserCached = userLink.innerHTML;
this.loggedInUserHashCached = document.querySelector('[name=uh]').value;
} else {
if (tryingEarly) {
// trying early means we're trying before DOM load may be complete, so if we fail here
// we don't want to null this, we want to allow another try.
// currently the only place this is really used is username hider, which tries (if possible)
// to hide the username as early/fast as possible.
delete this.loggedInUserCached;
delete this.loggedInUserHashCached;
} else {
this.loggedInUserCached = null;
}
}
}
return this.loggedInUserCached;
},
loggedInUserHash: function() {
this.loggedInUser();
return this.loggedInUserHashCached;
},
getUserInfo: function(callback, username, live) {
// Default to currently logged-in user, for backwards compatibility
username = (typeof username !== "undefined" ? username : RESUtils.loggedInUser());
if (username === null) return false;
// Default to getting live data (i.e. from reddit's server)
live = (typeof live === "boolean" ? live : true);
if (!(username in RESUtils.userInfoCallbacks)) {
RESUtils.userInfoCallbacks[username] = [];
}
RESUtils.userInfoCallbacks[username].push(callback);
var cacheData = RESStorage.getItem('RESUtils.userInfoCache.' + username) || '{}';
var userInfoCache = safeJSON.parse(cacheData);
var lastCheck = (userInfoCache !== null) ? parseInt(userInfoCache.lastCheck, 10) || 0 : 0;
var now = new Date();
// 300000 = 5 minutes
if (live && (now.getTime() - lastCheck) > 300000) {
if (!RESUtils.userInfoRunning) {
RESUtils.userInfoRunning = true;
GM_xmlhttpRequest({
method: "GET",
url: location.protocol + "//" + location.hostname + "/user/" + encodeURIComponent(username) + "/about.json?app=res",
onload: function(response) {
var thisResponse = JSON.parse(response.responseText);
var userInfoCache = {
lastCheck: now.getTime(),
userInfo: thisResponse
};
RESStorage.setItem('RESUtils.userInfoCache.' + username, JSON.stringify(userInfoCache));
while (RESUtils.userInfoCallbacks[username].length > 0) {
var thisCallback = RESUtils.userInfoCallbacks[username].pop();
thisCallback(userInfoCache.userInfo);
}
RESUtils.userInfoRunning = false;
}
});
}
} else {
while (RESUtils.userInfoCallbacks[username].length > 0) {
var thisCallback = RESUtils.userInfoCallbacks[username].pop();
thisCallback(userInfoCache.userInfo);
}
}
},
userInfoCallbacks: {},
commentsRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*comments\/?[-\w\.\/]*/i,
friendsCommentsRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/r\/friends\/*comments\/?/i,
inboxRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/message\/[-\w\.\/]*/i,
profileRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.#=]*\/?(comments)?\/?(\?([a-z]+=[a-zA-Z0-9_%]*&?)*)?$/i, // fix to regex contributed by s_quark
submitRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/([-\w\.\/]*\/)?submit\/?(\?.*)?$/i,
prefsRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/prefs\/?/i,
wikiRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\.]+\/wiki?/i,
pageType: function() {
if (typeof this.pageTypeSaved === 'undefined') {
var pageType = '';
var currURL = location.href.split('#')[0];
if (RESUtils.profileRegex.test(currURL)) {
pageType = 'profile';
} else if ((RESUtils.commentsRegex.test(currURL)) || (RESUtils.friendsCommentsRegex.test(currURL))) {
pageType = 'comments';
} else if (RESUtils.inboxRegex.test(currURL)) {
pageType = 'inbox';
} else if (RESUtils.submitRegex.test(currURL)) {
pageType = 'submit';
} else if (RESUtils.prefsRegex.test(currURL)) {
pageType = 'prefs';
} else if (RESUtils.wikiRegex.test(currURL)) {
pageType = 'wiki';
} else {
pageType = 'linklist';
}
this.pageTypeSaved = pageType;
}
return this.pageTypeSaved;
},
commentPermalinkRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*comments\/[a-z0-9]+\/[^\/]+\/[a-z0-9]+\/?$/i,
isCommentPermalinkPage: function() {
if (typeof this.isCommentPermalinkSaved === 'undefined') {
var currURL = location.href.split('#')[0];
if (RESUtils.commentPermalinkRegex.test(currURL)) {
this.isCommentPermalinkSaved = true;
} else {
this.isCommentPermalinkSaved = false;
}
}
return this.isCommentPermalinkSaved;
},
matchRE: /^https?:\/\/(?:[a-z]+)\.reddit\.com\/r\/([\w\.\+]+).*/i,
matchDOM: /^https?:\/\/(?:[a-z]+)\.reddit\.com\/domain\/([\w\.\+]+).*/i,
currentSubreddit: function(check) {
if (typeof this.curSub === 'undefined') {
var match = location.href.match(RESUtils.matchRE);
if (match !== null) {
this.curSub = match[1];
if (check) return (match[1].toLowerCase() === check.toLowerCase());
return match[1];
} else {
if (check) return false;
return null;
}
} else {
if (check) return (this.curSub.toLowerCase() === check.toLowerCase());
return this.curSub;
}
},
currentDomain: function(check) {
if (typeof this.curDom === 'undefined') {
var match = location.href.match(RESUtils.matchDOM);
if (match !== null) {
this.curDom = match[1];
if (check) return (match[1].toLowerCase() === check.toLowerCase());
return match[1];
} else {
if (check) return false;
return null;
}
} else {
if (check) return (this.curDom.toLowerCase() === check.toLowerCase());
return this.curDom;
}
},
currentUserProfile: function() {
if (typeof this.curUserProfile === 'undefined') {
var match = location.href.match(/^https?:\/\/(?:[a-z]+)\.reddit\.com\/user\/([\w\.]+).*/i);
if (match !== null) {
this.curUserProfile = match[1];
return match[1];
} else {
return null;
}
} else {
return this.curUserProfile;
}
},
getXYpos: function (obj) {
var topValue= 0,leftValue= 0;
while(obj) {
leftValue += obj.offsetLeft;
topValue += obj.offsetTop;
obj = obj.offsetParent;
}
return { 'x': leftValue, 'y': topValue };
},
elementInViewport: function (obj) {
// check the headerOffset - if we've pinned the subreddit bar, we need to add some pixels so the "visible" stuff is lower down the page.
var headerOffset = this.getHeaderOffset();
var top = obj.offsetTop - headerOffset;
var left = obj.offsetLeft;
var width = obj.offsetWidth;
var height = obj.offsetHeight;
while(obj.offsetParent) {
obj = obj.offsetParent;
top += obj.offsetTop;
left += obj.offsetLeft;
}
return (
top >= window.pageYOffset &&
left >= window.pageXOffset &&
(top + height) <= (window.pageYOffset + window.innerHeight - headerOffset) &&
(left + width) <= (window.pageXOffset + window.innerWidth)
);
},
setMouseXY: function(e) {
e = e || window.event;
var cursor = {x:0, y:0};
if (e.pageX || e.pageY) {
cursor.x = e.pageX;
cursor.y = e.pageY;
} else {
cursor.x = e.clientX +
(document.documentElement.scrollLeft ||
document.body.scrollLeft) -
document.documentElement.clientLeft;
cursor.y = e.clientY +
(document.documentElement.scrollTop ||
document.body.scrollTop) -
document.documentElement.clientTop;
}
RESUtils.mouseX = cursor.x;
RESUtils.mouseY = cursor.y;
},
elementUnderMouse: function ( obj ) {
var $obj = $(obj),
top = $obj.offset().top,
left = $obj.offset().left,
width = $obj.outerWidth(),
height = $obj.outerHeight(),
right = left + width,
bottom = top + height;
if ((RESUtils.mouseX >= left) && (RESUtils.mouseX <= right) && (RESUtils.mouseY >= top) && (RESUtils.mouseY <= bottom)) {
return true;
} else {
return false;
}
},
doElementsCollide: function (ele1, ele2, margin) {
margin = margin || 0;
ele1 = $(ele1);
ele2 = $(ele2);
var dims1 = ele1.offset();
dims1.right = dims1.left + ele1.width();
dims1.bottom = dims1.top + ele1.height();
dims1.left -= margin;
dims1.top -= margin;
dims1.right += margin;
dims1.bottom += margin;
var dims2 = ele2.offset();
dims2.right = dims2.left + ele2.width();
dims2.bottom = dims2.top + ele2.height();
if (
(
(dims1.left < dims2.left && dims2.left < dims1.right) ||
(dims1.left < dims2.right && dims2.right < dims1.right) ||
(dims2.left < dims1.left && dims1.left < dims2.right) ||
(dims2.left < dims1.right && dims1.right < dims2.right)
) &&
(
(dims1.top < dims2.top && dims2.top < dims1.bottom) ||
(dims1.top < dims2.bottom && dims2.bottom < dims1.bottom) ||
(dims2.top < dims1.top && dims1.top < dims2.bottom) ||
(dims2.top < dims1.bottom && dims1.bottom < dims2.bottom))
)
{
// In layman's terms:
// If one of the box's left/right borders is between the other box's left/right
// and same with top/bottom,
// then they collide.
// This could probably be logicked into a more compact form.
return true;
}
return false;
},
scrollTo: function(x,y) {
var headerOffset = this.getHeaderOffset();
window.scrollTo(x,y-headerOffset);
},
getHeaderOffset: function() {
if (typeof this.headerOffset === 'undefined') {
this.headerOffset = 0;
switch (modules['betteReddit'].options.pinHeader.value) {
case 'none':
break;
case 'sub':
this.theHeader = document.querySelector('#sr-header-area');
break;
case 'subanduser':
this.theHeader = document.querySelector('#sr-header-area');
break;
case 'header':
this.theHeader = document.querySelector('#header');
break;
}
if (this.theHeader) {
this.headerOffset = this.theHeader.offsetHeight + 6;
}
}
return this.headerOffset;
},
setSelectValue: function(obj, value) {
for (var i=0, len=obj.length; i < len; i++) {
// for some reason in firefox, obj[0] is undefined... weird. adding a test for existence of obj[i]...
// okay, now as of ff8, it's even barfing here unless we console.log out a check - nonsensical.
// a bug has been filed to bugzilla at:
// https://bugzilla.mozilla.org/show_bug.cgi?id=702847
if ((obj[i]) && (obj[i].value == value)) {
obj[i].selected = true;
}
}
},
stripHTML: function(str) {
var regExp = /<\/?[^>]+>/gi;
str = str.replace(regExp, "");
return str;
},
sanitizeHTML: function(htmlStr) {
if (!this.sanitizer) {
var SnuOwnd = window.SnuOwnd;
var redditCallbacks = SnuOwnd.getRedditCallbacks();
var callbacks = SnuOwnd.createCustomCallbacks({
paragraph: function(out, text, options){
if (text) out.s += text.s;
},
autolink: redditCallbacks.autolink,
raw_html_tag: redditCallbacks.raw_html_tag
});
var rendererConfig = SnuOwnd.defaultRenderState();
rendererConfig.flags = SnuOwnd.DEFAULT_WIKI_FLAGS;
rendererConfig.html_element_whitelist = [
'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'div', 'code',
'br', 'hr', 'p', 'a', 'img', 'pre', 'blockquote', 'table',
'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'strong', 'em',
'i', 'b', 'u', 'ul', 'ol', 'li', 'dl', 'dt', 'dd',
'font', 'center', 'small', 's', 'q', 'sub', 'sup', 'del'
];
rendererConfig.html_attr_whitelist = [
'href', 'title', 'src', 'alt', 'colspan',
'rowspan', 'cellspacing', 'cellpadding', 'scope',
'face', 'color', 'size', 'bgcolor', 'align'
];
this.sanitizer = SnuOwnd.getParser({
callbacks: callbacks,
context: rendererConfig
});
}
return this.sanitizer.render(htmlStr);
},
fadeElementOut: function(obj, speed, callback) {
if (obj.getAttribute('isfading') === 'in') {
return false;
}
obj.setAttribute('isfading','out');
speed = speed || 0.1;
if (obj.style.opacity === '') obj.style.opacity = '1';
if (obj.style.opacity <= 0) {
obj.style.display = 'none';
obj.setAttribute('isfading',false);
if (callback) callback();
return true;
} else {
var newOpacity = parseFloat(obj.style.opacity) - speed;
if (newOpacity < speed) newOpacity = 0;
obj.style.opacity = newOpacity;
setTimeout(function() { RESUtils.fadeElementOut(obj, speed, callback); }, 100);
}
},
fadeElementIn: function(obj, speed, finalOpacity) {
finalOpacity = finalOpacity || 1;
if (obj.getAttribute('isfading') === 'out') {
return false;
}
obj.setAttribute('isfading','in');
speed = speed || 0.1;
if ((obj.style.display === 'none') || (obj.style.display === '')) {
obj.style.opacity = 0;
obj.style.display = 'block';
}
if (obj.style.opacity >= finalOpacity) {
obj.setAttribute('isfading',false);
obj.style.opacity = finalOpacity;
return true;
} else {
var newOpacity = parseFloat(obj.style.opacity) + parseFloat(speed);
if (newOpacity > finalOpacity) newOpacity = finalOpacity;
obj.style.opacity = newOpacity;
setTimeout(function() { RESUtils.fadeElementIn(obj, speed, finalOpacity); }, 100);
}
},
setCursorPosition: function(form, pos) {
elem = $(form)[0];
if (!elem) return;
if (elem.setSelectionRange) {
elem.setSelectionRange(pos, pos);
} else if (elem.createTextRange) {
var range = elem.createTextRange();
range.collapse(true);
range.moveEnd('character', pos);
range.moveStart('character', pos);
range.select();
}
return form;
},
setNewNotification: function() {
$('#RESSettingsButton, #RESMainGearOverlay .gearIcon').addClass('newNotification').click(function() {
location.href = '/r/RESAnnouncements';
});
},
createMultiLock: function() {
var locks = {};
var count = 0;
return {
lock: function(lockname, value) {
if (typeof lockname === "undefined") return;
if (locks[lockname]) return;
locks[lockname] = value || true;
count++;
return true;
},
unlock: function(lockname) {
if (typeof lockname === "undefined") return;
if (!locks[lockname]) return;
locks[lockname] = false;
count--;
return true;
},
locked: function(lockname) {
if (typeof lockname !== "undefined") {
// Is this lock set?
return locks[lockname];
} else {
// Is any lock set?
return count > 0;
}
}
};
},
indexOptionTable: function(moduleID, optionKey, keyFieldIndex) {
var source = modules[moduleID].options[optionKey].value;
var keyIsList =
modules[moduleID].options[optionKey].fields[keyFieldIndex].type === 'list' ?
',' :
false;
return RESUtils.indexArrayByProperty(source, keyFieldIndex, keyIsList);
},
indexArrayByProperty: function(source, keyIndex, keyValueSeparator) {
if (!source || !source.length) {
return function() { };
}
var index = createIndex();
return getItem;
function createIndex() {
var index = {};
for (var i = 0, length = source.length; i < length; i++) {
var item = source[i];
var key = item && item[keyIndex];
if (!key) continue;
if (keyValueSeparator) {
var keys = key.toLowerCase().split(keyValueSeparator);
for (var ki = 0, klength = keys.length; ki < klength; ki++) {
key = keys[ki];
index[key] = item;
}
} else {
index[key] = item;
}
}
return index;
}
function getItem(key) {
key = key && key.toLowerCase();
var item = index[key];
return item;
}
},
inList: function(needle, haystack, separator, isCaseSensitive) {
if (!needle || !haystack) return false;
separator = separator || ',';
if (haystack.indexOf(separator) !== -1) {
var haystacks = haystack.split(separator);
if (RESUtils.inArray(needle, haystacks, isCaseSensitive)) {
return true;
}
} else {
if (caseSensitive) {
return (needle == haystack);
} else {
return (needle.toLowerCase() == haystack.toLowerCase());
}
}
},
inArray: function(needle, haystacks, isCaseSensitive) {
if (!isCaseSensitive) needle = needle.toLowerCase();
for (var i = 0, length = haystacks.length; i < length; i++) {
if (isCaseSensitive) {
if (needle == haystacks[i]) {
return true;
}
} else {
if (needle == haystacks[i].toLowerCase()) {
return true;
}
}
}
},
firstRun: function() {
// if this is the first time this version has been run, pop open the what's new tab, background focused.
if (RESStorage.getItem('RES.firstRun.'+RESVersion) === null) {
RESStorage.setItem('RES.firstRun.'+RESVersion,'true');
RESUtils.openLinkInNewTab('http://redditenhancementsuite.com/whatsnew.html?v='+RESVersion, false);
}
},
// checkForUpdate: function(forceUpdate) {
checkForUpdate: function() {
if (RESUtils.currentSubreddit('RESAnnouncements')) {
RESStorage.removeItem('RES.newAnnouncement','true');
}
var now = new Date();
var lastCheck = parseInt(RESStorage.getItem('RESLastUpdateCheck'), 10) || 0;
// if we haven't checked for an update in 24 hours, check for one now!
// if (((now.getTime() - lastCheck) > 86400000) || (RESVersion > RESStorage.getItem('RESlatestVersion')) || ((RESStorage.getItem('RESoutdated') === 'true') && (RESVersion == RESStorage.getItem('RESlatestVersion'))) || forceUpdate) {
if ((now.getTime() - lastCheck) > 86400000) {
// now we're just going to check /r/RESAnnouncements for new posts, we're not checking version numbers...
var lastID = RESStorage.getItem('RES.lastAnnouncementID');
$.getJSON('/r/RESAnnouncements/.json?limit=1&app=res', function(data) {
RESStorage.setItem('RESLastUpdateCheck',now.getTime());
var thisID = data.data.children[0].data.id;
if (thisID != lastID) {
RESStorage.setItem('RES.newAnnouncement','true');
RESUtils.setNewNotification();
}
RESStorage.setItem('RES.lastAnnouncementID', thisID);
});
/*
var jsonURL = 'http://reddit.honestbleeps.com/update.json?v=' + RESVersion;
// mark off that we've checked for an update...
RESStorage.setItem('RESLastUpdateCheck',now.getTime());
var outdated = false;
if (BrowserDetect.isChrome()) {
// we've got chrome, so we need to hit up the background page to do cross domain XHR
var thisJSON = {
requestType: 'compareVersion',
url: jsonURL
};
chrome.extension.sendMessage(thisJSON, function(response) {
// send message to background.html to open new tabs...
outdated = RESUtils.compareVersion(response, forceUpdate);
});
} else if (BrowserDetect.isSafari()) {
// we've got safari, so we need to hit up the background page to do cross domain XHR
thisJSON = {
requestType: 'compareVersion',
url: jsonURL,
forceUpdate: forceUpdate
}
safari.self.tab.dispatchMessage("compareVersion", thisJSON);
} else if (BrowserDetect.isOpera()) {
// we've got opera, so we need to hit up the background page to do cross domain XHR
thisJSON = {
requestType: 'compareVersion',
url: jsonURL,
forceUpdate: forceUpdate
}
opera.extension.postMessage(JSON.stringify(thisJSON));
} else {
// we've got greasemonkey, so we can do cross domain XHR.
GM_xmlhttpRequest({
method: "GET",
url: jsonURL,
onload: function(response) {
outdated = RESUtils.compareVersion(JSON.parse(response.responseText), forceUpdate);
}
});
}
*/
}
},
/*
compareVersion: function(response, forceUpdate) {
if (RESVersion < response.latestVersion) {
RESStorage.setItem('RESoutdated','true');
RESStorage.setItem('RESlatestVersion',response.latestVersion);
RESStorage.setItem('RESmessage',response.message);
if (forceUpdate) {
$(RESConsole.RESCheckUpdateButton).html('You are out of date! [click to update]');
}
return true;
} else {
RESStorage.setItem('RESlatestVersion',response.latestVersion);
RESStorage.setItem('RESoutdated','false');
if (forceUpdate) {
$(RESConsole.RESCheckUpdateButton).html('You are up to date!');
}
return false;
}
},
*/
proEnabled: function() {
return ((typeof modules['RESPro'] !== 'undefined') && (modules['RESPro'].isEnabled()));
},
niceKeyCode: function(charCode) {
var keyComboString = '';
var testCode, niceString;
if (typeof charCode === 'string') {
var tempArray = charCode.split(',');
if (tempArray.length) {
if (tempArray[1] === 'true') keyComboString += 'alt-';
if (tempArray[2] === 'true') keyComboString += 'ctrl-';
if (tempArray[3] === 'true') keyComboString += 'shift-';
if (tempArray[4] === 'true') keyComboString += 'command-';
}
testCode = parseInt(charCode, 10);
} else if (typeof charCode === 'object') {
testCode = parseInt(charCode[0], 10);
if (charCode[1]) keyComboString += 'alt-';
if (charCode[2]) keyComboString += 'ctrl-';
if (charCode[3]) keyComboString += 'shift-';
if (charCode[4]) keyComboString += 'command-';
}
switch(testCode) {
case 8:
niceString = "backspace"; // backspace
break;
case 9:
niceString = "tab"; // tab
break;
case 13:
niceString = "enter"; // enter
break;
case 16:
niceString = "shift"; // shift
break;
case 17:
niceString = "ctrl"; // ctrl
break;
case 18:
niceString = "alt"; // alt
break;
case 19:
niceString = "pause/break"; // pause/break
break;
case 20:
niceString = "caps lock"; // caps lock
break;
case 27:
niceString = "escape"; // escape
break;
case 33:
niceString = "page up"; // page up, to avoid displaying alternate character and confusing people
break;
case 34:
niceString = "page down"; // page down
break;
case 35:
niceString = "end"; // end
break;
case 36:
niceString = "home"; // home
break;
case 37:
niceString = "left arrow"; // left arrow
break;
case 38:
niceString = "up arrow"; // up arrow
break;
case 39:
niceString = "right arrow"; // right arrow
break;
case 40:
niceString = "down arrow"; // down arrow
break;
case 45:
niceString = "insert"; // insert
break;
case 46:
niceString = "delete"; // delete
break;
case 91:
niceString = "left window"; // left window
break;
case 92:
niceString = "right window"; // right window
break;
case 93:
niceString = "select key"; // select key
break;
case 96:
niceString = "numpad 0"; // numpad 0
break;
case 97:
niceString = "numpad 1"; // numpad 1
break;
case 98:
niceString = "numpad 2"; // numpad 2
break;
case 99:
niceString = "numpad 3"; // numpad 3
break;
case 100:
niceString = "numpad 4"; // numpad 4
break;
case 101:
niceString = "numpad 5"; // numpad 5
break;
case 102:
niceString = "numpad 6"; // numpad 6
break;
case 103:
niceString = "numpad 7"; // numpad 7
break;
case 104:
niceString = "numpad 8"; // numpad 8
break;
case 105:
niceString = "numpad 9"; // numpad 9
break;
case 106:
niceString = "multiply"; // multiply
break;
case 107:
niceString = "add"; // add
break;
case 109:
niceString = "subtract"; // subtract
break;
case 110:
niceString = "decimal point"; // decimal point
break;
case 111:
niceString = "divide"; // divide
break;
case 112:
niceString = "F1"; // F1
break;
case 113:
niceString = "F2"; // F2
break;
case 114:
niceString = "F3"; // F3
break;
case 115:
niceString = "F4"; // F4
break;
case 116:
niceString = "F5"; // F5
break;
case 117:
niceString = "F6"; // F6
break;
case 118:
niceString = "F7"; // F7
break;
case 119:
niceString = "F8"; // F8
break;
case 120:
niceString = "F9"; // F9
break;
case 121:
niceString = "F10"; // F10
break;
case 122:
niceString = "F11"; // F11
break;
case 123:
niceString = "F12"; // F12
break;
case 144:
niceString = "num lock"; // num lock
break;
case 145:
niceString = "scroll lock"; // scroll lock
break;
case 186:
niceString = ";"; // semi-colon
break;
case 187:
niceString = "="; // equal-sign
break;
case 188:
niceString = ","; // comma
break;
case 189:
niceString = "-"; // dash
break;
case 190:
niceString = "."; // period
break;
case 191:
niceString = "/"; // forward slash
break;
case 192:
niceString = "`"; // grave accent
break;
case 219:
niceString = "["; // open bracket
break;
case 220:
niceString = "\\"; // back slash
break;
case 221:
niceString = "]"; // close bracket
break;
case 222:
niceString = "'"; // single quote
break;
default:
niceString = String.fromCharCode(testCode);
break;
}
return keyComboString + niceString;
},
niceDate: function(d, usformat) {
d = d || new Date();
var year = d.getFullYear();
var month = (d.getMonth() + 1);
month = (month < 10) ? '0'+month : month;
var day = d.getDate();
day = (day < 10) ? '0'+day : day;
var fullString = year+'-'+month+'-'+day;
if (usformat) {
fullString = month+'-'+day+'-'+year;
}
return fullString;
},
niceDateTime: function(d, usformat) {
d = d || new Date();
var dateString = RESUtils.niceDate(d);
var hours = d.getHours();
hours = (hours < 10) ? '0'+hours : hours;
var minutes = d.getMinutes();
minutes = (minutes < 10) ? '0'+minutes : minutes;
var seconds = d.getSeconds();
seconds = (seconds < 10) ? '0'+seconds : seconds;
var fullString = dateString + ' ' + hours + ':'+minutes+':'+seconds;
return fullString;
},
niceDateDiff: function(origdate, newdate) {
// Enter the month, day, and year below you want to use as
// the starting point for the date calculation
if (!newdate) {
newdate = new Date();
}
var amonth = origdate.getUTCMonth() + 1;
var aday = origdate.getUTCDate();
var ayear = origdate.getUTCFullYear();
var tyear = newdate.getUTCFullYear();
var tmonth = newdate.getUTCMonth() + 1;
var tday = newdate.getUTCDate();
var y = 1;
var mm = 1;
var d = 1;
var a2 = 0;
var a1 = 0;
var f = 28;
if (((tyear % 4 === 0) && (tyear % 100 !== 0)) || (tyear % 400 === 0)) {
f = 29;
}
var m = [31, f, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
var dyear = tyear - ayear;
var dmonth = tmonth - amonth;
if (dmonth < 0 && dyear > 0) {
dmonth = dmonth + 12;
dyear--;
}
var dday = tday - aday;
if (dday < 0) {
if (dmonth > 0) {
var ma = amonth + tmonth;
if (ma >= 12) { ma = ma - 12; }
if (ma < 0) { ma = ma + 12; }
dday = dday + m[ma];
dmonth--;
if (dmonth < 0) {
dyear--;
dmonth = dmonth + 12;
}
} else {
dday = 0;
}
}
var returnString = '';
if (dyear === 0) { y = 0; }
if (dmonth === 0) { mm = 0; }
if (dday === 0) { d = 0; }
if ((y === 1) && (mm === 1)) { a1 = 1; }
if ((y === 1) && (d === 1)) { a1 = 1; }
if ((mm === 1) && (d === 1)) { a2 = 1; }
if (y === 1){
if (dyear === 1) {
returnString += dyear + " year";
} else {
returnString += dyear + " years";
}
}
if ((a1 === 1) && (a2 === 0)) { returnString += " and "; }
if ((a1 === 1) && (a2 === 1)) { returnString += ", "; }
if (mm === 1){
if (dmonth === 1) {
returnString += dmonth + " month";
} else {
returnString += dmonth + " months";
}
}
if (a2 === 1) { returnString += " and "; }
if (d === 1) {
if (dday === 1) {
returnString += dday + " day";
} else {
returnString += dday + " days";
}
}
if (returnString === '') {
returnString = '0 days';
}
return returnString;
},
checkIfSubmitting: function() {
this.checkedIfSubmitting = true;
if ((location.href.match(/\/r\/[\w]+\/submit\/?/i)) || (location.href.match(/reddit\.com\/submit\/?/i))) {
var thisSubRedditInput = document.getElementById('sr-autocomplete');
if (thisSubRedditInput) {
var thisSubReddit = thisSubRedditInput.value;
var title = document.querySelector('textarea[name=title]');
if (typeof this.thisSubRedditInputListener === 'undefined') {
this.thisSubRedditInputListener = true;
thisSubRedditInput.addEventListener('change', function(e) {
RESUtils.checkIfSubmitting();
}, false);
}
if ((thisSubReddit.toLowerCase() === 'enhancement') || (thisSubReddit.toLowerCase() === 'resissues')) {
RESUtils.addCSS('#submittingToEnhancement { display: none; min-height: 300px; font-size: 14px; line-height: 15px; margin-top: 10px; width: 518px; position: absolute; z-index: 999; } #submittingToEnhancement ol { margin-left: 10px; margin-top: 15px; list-style-type: decimal; } #submittingToEnhancement li { margin-left: 25px; }');
RESUtils.addCSS('.submittingToEnhancementButton { border: 1px solid #444; border-radius: 2px; padding: 3px 6px; cursor: pointer; display: inline-block; margin-top: 12px; }');
RESUtils.addCSS('#RESBugReport, #RESFeatureRequest { display: none; }');
RESUtils.addCSS('#RESSubmitOptions .submittingToEnhancementButton { margin-top: 30px; }');
var textDesc = document.getElementById('text-desc');
this.submittingToEnhancement = createElementWithID('div','submittingToEnhancement','RESDialogSmall');
/*jshint multistr: true */
var submittingHTML = " \
Submitting to r/Enhancement
\
\
\
What kind of a post do you want to submit to r/Enhancement? So that we can better support you, please choose from the options below, and please take care to read the instructions, thanks! \
I want to submit a bug report
\
I want to submit a feature request
\
I want to submit a general question or other item
\
\
\
Are you sure you want to submit a bug report? We get a lot of duplicates and it would really help if you took a moment to read the following: \
\
Have you searched /r/RESIssues to see if someone else has reported it?
Are you sure it's a bug with RES specifically? Do you have any other userscripts/extensions running? How about addons like BetterPrivacy, Ghostery, CCleaner, etc?
\
\
\
Please also check out the latest known / popular bugs first: \
Loading...
\
I still want to submit a bug! \
\
\
So you want to request a feature, great! Please just consider the following, first: \
\
Have you searched /r/Enhancement to see if someone else has requested it?
\
Is it something that would appeal to Reddit as a whole? Personal or subreddit specific requests usually aren't added to RES.
\
\
\
Please also check out the latest known popular feature requests first: \
');
});
});
}
);
}
);
$('#submittingBug').click(
function() {
$('#sr-autocomplete').val('RESIssues');
$('li a.text-button').click();
$('#submittingToEnhancement').fadeOut();
var txt = "- RES Version: " + RESVersion + "\n";
txt += "- Browser: " + BrowserDetect.browser + "\n";
if (typeof navigator === 'undefined') navigator = window.navigator;
txt+= "- Browser Version: " + BrowserDetect.version + "\n";
txt+= "- Cookies Enabled: " + navigator.cookieEnabled + "\n";
txt+= "- Platform: " + BrowserDetect.OS + "\n";
txt+= "- Did you search /r/RESIssues before submitting this: No. That, or I didn't notice this text here and edit it!\n\n";
$('.usertext-edit textarea').val(txt);
title.value = '[bug] Please describe your bug here. If you have screenshots, please link them in the selftext.';
}
);
$('#submittingFeature').click(
function() {
$('#sr-autocomplete').val('Enhancement');
$('#submittingToEnhancement').fadeOut();
title.value = '[feature request] Please summarize your feature request here, and elaborate in the selftext.';
}
);
$('#RESSubmitOther').click(
function() {
$('#sr-autocomplete').val('Enhancement');
$('#submittingToEnhancement').fadeOut();
title.value = '';
}
);
$('#submittingToEnhancement').fadeIn();
}, 1000);
} else if (typeof this.submittingToEnhancement !== 'undefined') {
this.submittingToEnhancement.parentNode.removeChild(this.submittingToEnhancement);
if (title.value === 'Submitting a bug? Please read the box above...') {
title.value = '';
}
}
}
}
},
isEmpty: function(obj) {
for(var prop in obj) {
if(obj.hasOwnProperty(prop))
return false;
}
return true;
},
deleteCookie: function(cookieName) {
var requestJSON = {
requestType: 'deleteCookie',
cname: cookieName
};
if (BrowserDetect.isChrome()) {
chrome.extension.sendMessage(requestJSON);
} else if (BrowserDetect.isSafari()) {
document.cookie = cookieName + '=null;expires=' + new Date() +'; path=/;domain=reddit.com';
} else if (BrowserDetect.isOpera()) {
document.cookie = cookieName + '=null;expires=' + new Date() +'; path=/;domain=reddit.com';
} else if (BrowserDetect.isFirefox()) {
self.postMessage(requestJSON);
}
},
openLinkInNewTab: function(url, focus) {
var thisJSON;
if (BrowserDetect.isChrome()) {
thisJSON = {
requestType: 'openLinkInNewTab',
linkURL: url,
button: focus
};
// send message to background.html to open new tabs...
chrome.extension.sendMessage(thisJSON);
} else if (BrowserDetect.isSafari()) {
thisJSON = {
requestType: 'openLinkInNewTab',
linkURL: url,
button: focus
};
safari.self.tab.dispatchMessage("openLinkInNewTab", thisJSON);
} else if (BrowserDetect.isOpera()) {
thisJSON = {
requestType: 'openLinkInNewTab',
linkURL: url,
button: focus
};
opera.extension.postMessage(JSON.stringify(thisJSON));
} else if (BrowserDetect.isFirefox()) {
thisJSON = {
requestType: 'openLinkInNewTab',
linkURL: url,
button: focus
};
self.postMessage(thisJSON);
} else {
window.open(url);
}
},
notification: function(contentObj, delay) {
var content;
if (typeof contentObj.message === 'undefined') {
if (typeof contentObj === 'string') {
content = contentObj;
} else {
return false;
}
} else {
content = contentObj.message;
}
var header;
if (contentObj.header) {
header = contentObj.header;
} else {
header = [];
if (contentObj.moduleID && modules[contentObj.moduleID]) {
header.push(modules[contentObj.moduleID].moduleName);
}
if (contentObj.type === 'error') {
header.push('Error');
} else {
header.push('Notification');
}
header = header.join(' ');
}
if (contentObj.moduleID && modules[contentObj.moduleID]) {
header += modules['settingsNavigation'].makeUrlHashLink(contentObj.moduleID, contentObj.optionKey, ' ', 'gearIcon');
}
if (typeof this.notificationCount === 'undefined') {
this.adFrame = document.body.querySelector('#ad-frame');
if (this.adFrame) {
this.adFrame.style.display = 'none';
}
this.notificationCount = 0;
this.notificationTimers = [];
this.RESNotifications = createElementWithID('div','RESNotifications');
document.body.appendChild(this.RESNotifications);
}
var thisNotification = document.createElement('div');
thisNotification.classList.add('RESNotification');
thisNotification.setAttribute('id','RESNotification-'+this.notificationCount);
$(thisNotification).html('
'+header+'
×
'+content+'
');
var thisNotificationCloseButton = thisNotification.querySelector('.RESNotificationClose');
thisNotificationCloseButton.addEventListener('click', function(e) {
var thisNotification = e.target.parentNode.parentNode;
RESUtils.closeNotification(thisNotification);
}, false);
this.setCloseNotificationTimer(thisNotification, delay);
this.RESNotifications.style.display = 'block';
this.RESNotifications.appendChild(thisNotification);
modules['styleTweaks'].setSRStyleToggleVisibility(false, 'notification');
RESUtils.fadeElementIn(thisNotification, 0.2, 1);
this.notificationCount++;
},
setCloseNotificationTimer: function(e, delay) {
delay = delay || 3000;
var thisNotification = (typeof e.currentTarget !== 'undefined') ? e.currentTarget : e;
var thisNotificationID = thisNotification.getAttribute('id').split('-')[1];
thisNotification.classList.add('timerOn');
clearTimeout(RESUtils.notificationTimers[thisNotificationID]);
var thisTimer = setTimeout(function() {
RESUtils.closeNotification(thisNotification);
}, delay);
RESUtils.notificationTimers[thisNotificationID] = thisTimer;
thisNotification.addEventListener('mouseover',RESUtils.cancelCloseNotificationTimer, false);
thisNotification.removeEventListener('mouseout',RESUtils.setCloseNotification,false);
},
cancelCloseNotificationTimer: function(e) {
var thisNotificationID = e.currentTarget.getAttribute('id').split('-')[1];
e.currentTarget.classList.remove('timerOn');
clearTimeout(RESUtils.notificationTimers[thisNotificationID]);
e.target.removeEventListener('mouseover',RESUtils.cancelCloseNotification,false);
e.currentTarget.addEventListener('mouseout',RESUtils.setCloseNotificationTimer, false);
},
closeNotification: function(ele) {
RESUtils.fadeElementOut(ele, 0.1, RESUtils.notificationClosed);
},
notificationClosed: function(ele) {
var notifications = RESUtils.RESNotifications.querySelectorAll('.RESNotification');
var destroyed = 0;
for (var i=0, len=notifications.length; i'+onText+''+offText+'');
thisToggle.addEventListener('click', function(e) {
var thisCheckbox = this.querySelector('input[type=checkbox]');
var enabled = thisCheckbox.checked;
thisCheckbox.checked = !enabled;
if (enabled) {
this.classList.remove('enabled');
} else {
this.classList.add('enabled');
}
}, false);
if (enabled) thisToggle.classList.add('enabled');
return thisToggle;
},
addCommas: function(nStr) {
nStr += '';
var x = nStr.split('.');
var x1 = x[0];
var x2 = x.length > 1 ? '.' + x[1] : '';
var rgx = /(\d+)(\d{3})/;
while (rgx.test(x1)) {
x1 = x1.replace(rgx, '$1' + ',' + '$2');
}
return x1 + x2;
},
generateTable: function(items, call, context) {
if (!items || !call) return;
// Sanitize single item into items array
if (!(items.length && typeof items !== "string")) items = [ items ];
var description = [];
description.push('
');
for (var i = 0; i < items.length; i++) {
var item = call(items[i], i, items, context);
if (typeof item === "string") {
description.push(item);
} else if (item.length) {
description = description.concat(item);
}
}
description.push('
');
description = description.join('\n');
return description;
},
xhrCache: function(operation) {
var thisJSON = {
requestType: 'XHRCache',
operation: operation
};
if (BrowserDetect.isChrome()) {
chrome.extension.sendMessage(thisJSON);
} else if (BrowserDetect.isSafari()) {
safari.self.tab.dispatchMessage('XHRCache', thisJSON);
} else if (BrowserDetect.isOpera()) {
opera.extension.postMessage(JSON.stringify(thisJSON));
} else if (BrowserDetect.isFirefox()) {
self.postMessage(thisJSON);
}
},
initObservers: function() {
var siteTable, observer;
if (RESUtils.pageType() !== 'comments') {
// initialize sitetable observer...
siteTable = document.querySelector('#siteTable');
var stMultiCheck = document.querySelectorAll('#siteTable');
if (stMultiCheck.length === 2) {
siteTable = stMultiCheck[1];
}
if (BrowserDetect.MutationObserver && siteTable) {
observer = new BrowserDetect.MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes[0].id.indexOf('siteTable') !== -1) {
// when a new sitetable is loaded, we need to add new observers for selftexts within that sitetable...
$(mutation.addedNodes[0]).find('.entry div.expando').each(function() {
RESUtils.addSelfTextObserver(this);
});
RESUtils.watchers.siteTable.forEach(function(callback) {
if (callback) callback(mutation.addedNodes[0]);
});
}
});
});
observer.observe(siteTable, {
attributes: false,
childList: true,
characterData: false
});
} else {
// Opera doesn't support MutationObserver - so we need this for Opera support.
if (siteTable) {
siteTable.addEventListener('DOMNodeInserted', function(event) {
if ((event.target.tagName === 'DIV') && (event.target.getAttribute('id') && event.target.getAttribute('id').indexOf('siteTable') !== -1)) {
RESUtils.watchers.siteTable.forEach(function(callback) {
if (callback) callback(event.target);
});
}
}, true);
}
}
} else {
// initialize sitetable observer...
siteTable = document.querySelector('.commentarea > .sitetable');
if (BrowserDetect.MutationObserver && siteTable) {
observer = new BrowserDetect.MutationObserver(function(mutations) {
mutations.forEach(function(mutation) {
if (mutation.addedNodes.length > 0 && mutation.addedNodes[0].classList.contains('thing')) {
var thing = mutation.addedNodes[0];
var newCommentEntry = thing.querySelector('.entry');
if (!$(newCommentEntry).data('alreadyDetected')) {
$(newCommentEntry).data('alreadyDetected', true);
$(thing).find('.child').each(function() {
RESUtils.addNewCommentFormObserver(this);
});
RESUtils.watchers.newComments.forEach(function(callback) {
if (callback) callback(newCommentEntry);
});
}
}
});
});
observer.observe(siteTable, {
attributes: false,
childList: true,
characterData: false
});
} else {
// Opera doesn't support MutationObserver - so we need this for Opera support.
if (siteTable) {
siteTable.addEventListener('DOMNodeInserted', RESUtils.mutationEventCommentHandler, false);
}
}
}
$('.entry div.expando').each(function() {
RESUtils.addSelfTextObserver(this);
});
// initialize new comments observers on demand, by first wiring up click listeners to "load more comments" buttons.
// on click, we'll add a mutation observer...
$('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
// initialize new comments forms observers on demand, by first wiring up click listeners to reply buttons.
// on click, we'll add a mutation observer...
// $('body').delegate('ul.flat-list li a[onclick*=reply]', 'click', RESUtils.addNewCommentFormObserver);
$('.thing .child').each(function() {
RESUtils.addNewCommentFormObserver(this);
});
},
// Opera doesn't support MutationObserver - so we need this for Opera support.
mutationEventCommentHandler: function (event) {
if ((event.target.tagName === 'DIV') && (event.target.classList.contains('thing'))) {
// we've found a matching element - stop propagation.
event.stopPropagation();
// because nested DOMNodeInserted events are an absolute CLUSTER to manage,
// only send individual comments through to the callback.
// Otherwise, we end up calling functions on a parent, then its child (which
// already got scanned when we passed in the parent), etc.
var thisComment = event.target.querySelector('.entry');
if (! $(thisComment).data('alreadyDetected')) {
$(thisComment).data('alreadyDetected', true);
// wire up listeners for new "more comments" links...
$(event.target).find('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
RESUtils.watchers.newComments.forEach(function(callback) {
RESUtils.addNewCommentFormObserver(event.target);
if (callback) callback(thisComment);
});
}
}
},
addNewCommentObserverToTarget: function (e) {
var ele = $(e.currentTarget).closest('.sitetable')[0];
// mark this as having an observer so we don't add multiples...
if (! $(ele).hasClass('hasObserver')) {
$(ele).addClass('hasObserver');
RESUtils.addNewCommentObserver(ele);
}
},
addNewCommentObserver: function(ele) {
var mutationNodeToObserve = ele;
if (BrowserDetect.MutationObserver) {
var observer = new BrowserDetect.MutationObserver(function(mutations) {
// we need to get ONLY the nodes that are new...
// get the nodeList from each mutation, find comments within it,
// then call our callback on it.
for (var i=0, len=mutations.length; i div.sitetable > .thing:first-child'); // assumes new comment will be prepended to sitetable's children
if ((newOwnComment) && (newOwnComment.length === 1)) {
// new comment detected from the current user...
RESUtils.watchers.newComments.forEach(function(callback) {
callback(newOwnComment[0]);
});
}
}
});
observer.observe(commentsFormParent, {
attributes: false,
childList: true,
characterData: false
});
} else {
// Opera doesn't support MutationObserver - so we need this for Opera support.
commentsFormParent.addEventListener('DOMNodeInserted', function(event) {
// TODO: proper tag filtering here, it's currently all wrong.
if (event.target.tagName === 'FORM') {
RESUtils.watchers.newCommentsForms.forEach(function(callback) {
if (callback) callback(event.target);
});
} else {
var newOwnComment = $(event.target).find(' > div.sitetable > .thing:first-child'); // assumes new comment will be prepended to sitetable's children
if ((newOwnComment) && (newOwnComment.length === 1)) {
// new comment detected from the current user...
RESUtils.watchers.newComments.forEach(function(callback) {
callback(newOwnComment[0]);
});
}
}
}, true);
}
},
addSelfTextObserver: function(ele) {
var selfTextParent = ele;
if (BrowserDetect.MutationObserver) {
// var mutationNodeToObserve = moreCommentsParent.parentNode.parentNode.parentNode.parentNode;
var observer = new BrowserDetect.MutationObserver(function(mutations) {
var form = $(mutations[0].target).find('form');
if ((form) && (form.length > 0)) {
RESUtils.watchers.selfText.forEach(function(callback) {
callback(form[0]);
});
}
});
observer.observe(selfTextParent, {
attributes: false,
childList: true,
characterData: false
});
} else {
// Opera doesn't support MutationObserver - so we need this for Opera support.
selfTextParent.addEventListener('DOMNodeInserted', function(event) {
// TODO: proper tag filtering here, it's currently all wrong.
if (event.target.tagName === 'FORM') {
RESUtils.watchers.selfText.forEach(function(callback) {
if (callback) callback(event.target);
});
}
}, true);
}
},
watchForElement: function(type, callback) {
switch(type) {
case 'siteTable':
RESUtils.watchers.siteTable.push(callback);
break;
case 'newComments':
RESUtils.watchers.newComments.push(callback);
break;
case 'selfText':
RESUtils.watchers.selfText.push(callback);
break;
case 'newCommentsForms':
RESUtils.watchers.newCommentsForms.push(callback);
break;
}
},
watchers: {
siteTable: [],
newComments: [],
selfText: [],
newCommentsForms: []
},
// A link is a comment code if all these conditions are true:
// * It has no content (i.e. content.length === 0)
// * Its href is of the form "/code"
//
// In case it's not clear, here is a list of some common comment
// codes on a specific subreddit:
// http://www.reddit.com/r/metarage/comments/p3eqe/full_updated_list_of_comment_faces_wcodes/
COMMENT_CODE_REGEX: /^\/\w+$/,
isCommentCode: function (link) {
var content = link.innerHTML;
// Note that link.href will return the full href (which includes the
// reddit.com domain). We don't want that.
var href = link.getAttribute("href");
return !content && this.COMMENT_CODE_REGEX.test(href);
},
/*
Starts a unique named timeout.
If there is a running timeout with the same name cancel the old one in favor of the new.
Call with no time/call parameter (null/undefined/missing) to and existing one with the given name.
Used to derfer an action until a series of events has stopped.
e.g. wait until a user a stopped typing to update a comment preview.
(name based on similar function in underscore.js)
*/
debounceTimeouts: {},
debounce: function(name, time, call, data) {
if (name == null) return;
if (RESUtils.debounceTimeouts[name] !== undefined) {
window.clearTimeout(RESUtils.debounceTimeouts[name]);
delete RESUtils.debounceTimeouts[name];
}
if (time !== null && call !== null) {
RESUtils.debounceTimeouts[name] = window.setTimeout(function() {
delete RESUtils.debounceTimeouts[name];
call(data);
}, time);
}
},
toolTipTimers: {},
/*
Iterate through an array in chunks, executing a callback on each element.
Each chunk is handled asynchronously from the others with a delay betwen each batch.
If the provided callback returns false iteration will be halted.
*/
forEachChunked: function(array, chunkSize, delay, call) {
if (typeof array === 'undefined' || array === null) return;
if (typeof chunkSize === 'undefined' || chunkSize === null || chunkSize < 1) return;
if (typeof delay === 'undefined' || delay === null || delay < 0) return;
if (typeof call === 'undefined' || call === null) return;
var counter = 0;
var length = array.length;
function doChunk() {
for (var end = Math.min(array.length, counter+chunkSize); counter < end; counter++) {
var ret = call(array[counter], counter, array);
if (ret === false) return;
}
if (counter < array.length) {
window.setTimeout(doChunk, delay);
}
}
window.setTimeout(doChunk, delay);
},
getComputedStyle: function(elem, property){
if (elem.constructor === String) {
elem = document.querySelector(elem);
} else if (!(elem instanceof Node)) {
return undefined;
}
var strValue;
if(document.defaultView && document.defaultView.getComputedStyle) {
strValue = document.defaultView.getComputedStyle(elem, "").getPropertyValue(property);
} else if(elem.currentStyle){
property = property.replace(/\-(\w)/g, function(strMatch, p1){
return p1.toUpperCase();
});
strValue = oElm.currentStyle[property];
}
return strValue;
},
hover: {
defaults: {
openDelay: 500,
fadeDelay: 500,
fadeSpeed: 0.3,
width: 512,
closeOnMouseOut: true
},
container: null,
/*
The contents of state are as follows:
state: {
//The DOM element that triggered the hover popup.
element: null,
//Resolved values for timing, etc.
options: null,
//Usecase specific object
context: null,
callback: null,
}*/
state: null,
showTimer: null,
hideTimer: null,
begin: function(onElement, conf, callback, context) {
var hover = RESUtils.hover;
if (hover.container === null) hover.create();
if (hover.state !== null) {
hover.close(false);
}
var state = hover.state = {
element: onElement,
options: $.extend({}, hover.defaults, conf),
context: context,
callback: callback,
};
hover.showTimer = setTimeout(function() {
hover.cancelShowTimer();
hover.clearShowListeners();
hover.open();
hover.state.element.addEventListener('mouseout', hover.startHideTimer, false);
}, state.options.openDelay);
state.element.addEventListener('click', hover.cancelShow, false);
state.element.addEventListener('mouseout', hover.cancelShow, false);
},
create: function() {
var container = $('
\
\
x
\
\
').get(0);
document.body.appendChild(container);
$(container).hover(function() {
if (RESUtils.hover.state !== null) {
RESUtils.hover.cancelHideTimer();
}
}, function() {
if (RESUtils.hover.state !== null) {
RESUtils.hover.cancelHideTimer();
if (RESUtils.hover.state.options.closeOnMouseOut === true) {
RESUtils.hover.startHideTimer();
}
}
});
$(container).on('click', '.RESCloseButton', function() {
RESUtils.hover.close(true);
});
RESUtils.hover.container = container;
var css = '';
css += '#RESHoverContainer { display: none; position: absolute; z-index: 10001; }';
css += '#RESHoverContainer:before { content: ""; position: absolute; top: 10px; left: -26px; border-style: solid; border-width: 10px 29px 10px 0; border-color: transparent #c7c7c7; display: block; width: 0; z-index: 1; }';
css += '#RESHoverContainer:after { content: ""; position: absolute; top: 10px; left: -24px; border-style: solid; border-width: 10px 29px 10px 0; border-color: transparent #f0f3fc; display: block; width: 0; z-index: 1; }';
css += '#RESHoverContainer.right:before { content: ""; position: absolute; top: 10px; right: -26px; left: auto; border-style: solid; border-width: 10px 0 10px 29px; border-color: transparent #c7c7c7; display: block; width: 0; z-index: 1; }';
css += '#RESHoverContainer.right:after { content: ""; position: absolute; top: 10px; right: -24px; left: auto; border-style: solid; border-width: 10px 0 10px 29px; border-color: transparent #f0f3fc; display: block; width: 0; z-index: 1; }';
RESUtils.addCSS(css);
},
open: function() {
var hover = RESUtils.hover;
var def = $.Deferred();
def.promise()
.progress(hover.set)
.done(hover.set)
.fail(hover.close);
hover.state.callback(def, hover.state.element, hover.state.context);
},
set: function(header, body) {
var hover = RESUtils.hover;
var container = hover.container;
if (header != null) $('#RESHoverTitle').empty().append(header);
if (body != null) $('#RESHoverBody').empty().append(body);
var XY=RESUtils.getXYpos(hover.state.element);
var width = $(hover.state.element).width();
var tooltipWidth = $(container).width();
tooltipWidth = hover.state.options.width;
RESUtils.fadeElementIn(hover.container, hover.state.options.fadeSpeed);
if((window.innerWidth-XY.x-width)<=tooltipWidth){
// tooltip would go off right edge - reverse it.
container.classList.add('right');
$(container).css({
top: XY.y - 14,
left: XY.x - tooltipWidth - 30,
width: tooltipWidth
});
} else {
container.classList.remove('right');
$(container).css({
top: XY.y - 14,
left: XY.x + width + 25,
width: tooltipWidth
});
}
},
cancelShow: function(e) {
RESUtils.hover.close(true);
},
clearShowListeners: function() {
if (RESUtils.hover.state === null) return;
var element = RESUtils.hover.state.element;
var func = RESUtils.hover.cancelShow;
element.removeEventListener('click', func, false);
element.removeEventListener('mouseout', func, false);
},
cancelShowTimer: function() {
if (RESUtils.hover.showTimer === null) return;
clearTimeout(RESUtils.hover.showTimer);
RESUtils.hover.showTimer = null;
},
startHideTimer: function() {
if (RESUtils.hover.state !== null) {
RESUtils.hover.hideTimer = setTimeout(function() {
RESUtils.hover.cancelHideTimer();
RESUtils.hover.close(true);
}, RESUtils.hover.state.options.fadeDelay);
}
},
cancelHideTimer: function() {
var hover = RESUtils.hover;
if (RESUtils.hover.state !== null) {
hover.state.element.removeEventListener('mouseout', hover.startHideTimer, false);
}
if (hover.hideTimer === null) return;
clearTimeout(hover.hideTimer);
hover.hideTimer = null;
},
close: function(fade) {
var hover = RESUtils.hover;
function afterHide() {
$('#RESHoverTitle, #RESHoverBody').empty();
hover.clearShowListeners();
hover.cancelShowTimer();
hover.cancelHideTimer();
hover.state = null;
}
if (fade && hover.state !== null) {
RESUtils.fadeElementOut(hover.container, hover.state.options.fadeSpeed, afterHide);
} else {
$(hover.container).hide(afterHide);
}
}
}
};
// end RESUtils;
// Create a nice alert function...
var gdAlert = {
container: false,
overlay: "",
init: function(callback) {
//init
var alertCSS = '#alert_message { ' +
'display: none;' +
'opacity: 0.0;' +
'background-color: #EFEFEF;' +
'border: 1px solid black;' +
'color: black;' +
'font-size: 10px;' +
'padding: 20px;' +
'padding-left: 60px;' +
'padding-right: 60px;' +
'position: fixed!important;' +
'position: absolute;' +
'width: 400px;' +
'float: left;' +
'z-index: 1000000201;' +
'text-align: left;' +
'left: auto;' +
'top: auto;' +
'}' +
'#alert_message .button {' +
'border: 1px solid black;' +
'font-weight: bold;' +
'font-size: 10px;' +
'padding: 4px;' +
'padding-left: 7px;' +
'padding-right: 7px;' +
'float: left;' +
'background-color: #DFDFDF;' +
'cursor: pointer;' +
'}' +
'#alert_message span {' +
'display: block;' +
'margin-bottom: 15px; ' +
'}'+
'#alert_message_background {' +
'position: fixed; top: 0; left: 0; bottom: 0; right: 0;' +
'background-color: #333333; z-index: 100000200;' +
'}';
GM_addStyle(alertCSS);
gdAlert.populateContainer(callback);
},
populateContainer: function(callback) {
gdAlert.container = createElementWithID('div','alert_message');
gdAlert.container.appendChild(document.createElement('span'));
if (typeof callback === 'function') {
this.okButton = document.createElement('input');
this.okButton.setAttribute('type','button');
this.okButton.setAttribute('value','confirm');
this.okButton.addEventListener('click',callback, false);
this.okButton.addEventListener('click',gdAlert.close, false);
var closeButton = document.createElement('input');
closeButton.setAttribute('type','button');
closeButton.setAttribute('value','cancel');
closeButton.addEventListener('click',gdAlert.close, false);
gdAlert.container.appendChild(this.okButton);
gdAlert.container.appendChild(closeButton);
} else {
/* if (this.okButton) {
gdAlert.container.removeChild(this.okButton);
delete this.okButton;
} */
var closeButton = document.createElement('input');
closeButton.setAttribute('type','button');
closeButton.setAttribute('value','ok');
closeButton.addEventListener('click',gdAlert.close, false);
gdAlert.container.appendChild(closeButton);
}
var br = document.createElement('br');
br.setAttribute('style','clear: both');
gdAlert.container.appendChild(br);
document.body.appendChild(gdAlert.container);
},
open: function(text, callback) {
if (gdAlert.isOpen) {
return;
}
gdAlert.isOpen = true;
gdAlert.populateContainer(callback);
//set message
// gdAlert.container.getElementsByTagName("SPAN")[0].innerHTML = text;
$(gdAlert.container.getElementsByTagName("SPAN")[0]).html(text);
gdAlert.container.getElementsByTagName("INPUT")[0].focus();
gdAlert.container.getElementsByTagName("INPUT")[0].focus();
//create site overlay
gdAlert.overlay = createElementWithID("div", "alert_message_background");
document.body.appendChild(gdAlert.overlay);
// center messagebox (requires prototype functions we don't have, so we'll redefine...)
// var arrayPageScroll = document.viewport.getScrollOffsets();
// var winH = arrayPageScroll[1] + (document.viewport.getHeight());
// var lightboxLeft = arrayPageScroll[0];
var arrayPageScroll = [ document.documentElement.scrollLeft , document.documentElement.scrollTop ];
var winH = arrayPageScroll[1] + (window.innerHeight);
var lightboxLeft = arrayPageScroll[0];
gdAlert.container.style.top = ((winH / 2) - 90) + "px";
gdAlert.container.style.left = ((gdAlert.getPageSize()[0] / 2) - 155) + "px";
/*
new Effect.Appear(gdAlert.container, {duration: 0.2});
new Effect.Opacity(gdAlert.overlay, {duration: 0.2, to: 0.8});
*/
RESUtils.fadeElementIn(gdAlert.container, 0.3);
RESUtils.fadeElementIn(gdAlert.overlay, 0.3);
modules['styleTweaks'].setSRStyleToggleVisibility(false, 'gdAlert');
},
close: function() {
gdAlert.isOpen = false;
/*
new Effect.Fade(gdAlert.container, {duration: 0.3});
new Effect.Fade(gdAlert.overlay, {duration: 0.3, afterFinish: function() {
document.body.removeChild(gdAlert.overlay);
}});
*/
RESUtils.fadeElementOut(gdAlert.container, 0.3);
RESUtils.fadeElementOut(gdAlert.overlay, 0.3);
modules['styleTweaks'].setSRStyleToggleVisibility(true, 'gdAlert');
},
getPageSize: function() {
var xScroll, yScroll;
if (window.innerHeight && window.scrollMaxY) {
xScroll = window.innerWidth + window.scrollMaxX;
yScroll = window.innerHeight + window.scrollMaxY;
} else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac
xScroll = document.body.scrollWidth;
yScroll = document.body.scrollHeight;
} else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
xScroll = document.body.offsetWidth;
yScroll = document.body.offsetHeight;
}
var windowWidth, windowHeight;
if (self.innerHeight) { // all except Explorer
if(document.documentElement.clientWidth){
windowWidth = document.documentElement.clientWidth;
} else {
windowWidth = self.innerWidth;
}
windowHeight = self.innerHeight;
} else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
windowWidth = document.documentElement.clientWidth;
windowHeight = document.documentElement.clientHeight;
} else if (document.body) { // other Explorers
windowWidth = document.body.clientWidth;
windowHeight = document.body.clientHeight;
}
// for small pages with total height less then height of the viewport
if(yScroll < windowHeight){
pageHeight = windowHeight;
} else {
pageHeight = yScroll;
}
// for small pages with total width less then width of the viewport
if(xScroll < windowWidth){
pageWidth = xScroll;
} else {
pageWidth = windowWidth;
}
return [pageWidth,pageHeight];
}
};
//overwrite the alert function
var alert = function(text, callback) {
if (gdAlert.container === false) {
gdAlert.init(callback);
}
gdAlert.open(text, callback);
};
// this function copies localStorage (from the GM import script) to FF addon simplestorage...
function GMSVtoFFSS() {
var console = unsafeWindow.console;
for (var key in localStorage) {
RESStorage.setItem(key, localStorage[key]);
}
localStorage.setItem('copyComplete','true');
localStorage.removeItem('RES.lsTest');
RESUtils.notification('Data transfer complete. You may now uninstall the Greasemonkey script');
}
// jquery plugin CSS
RESUtils.addCSS(tokenizeCSS);
RESUtils.addCSS(guidersCSS);
// define the RESConsole class
var RESConsole = {
modalOverlay: '',
RESConsoleContainer: '',
RESMenuItems: [],
RESConfigPanelOptions: null,
// make the modules panel accessible to this class for updating (i.e. when preferences change, so we can redraw it)
RESConsoleConfigPanel: createElementWithID('div', 'RESConsoleConfigPanel', 'RESPanel'),
RESConsoleAboutPanel: createElementWithID('div', 'RESConsoleAboutPanel', 'RESPanel'),
RESConsoleProPanel: createElementWithID('div', 'RESConsoleProPanel', 'RESPanel'),
addConsoleLink: function() {
this.userMenu = document.querySelector('#header-bottom-right');
if (this.userMenu) {
var RESPrefsLink = $("")
.mouseenter(RESConsole.showPrefsDropdown);
$(this.userMenu).find("ul").after(RESPrefsLink).after("|");
this.RESPrefsLink = RESPrefsLink[0];
}
},
addConsoleDropdown: function() {
this.gearOverlay = createElementWithID('div','RESMainGearOverlay');
this.gearOverlay.setAttribute('class','RESGearOverlay');
$(this.gearOverlay).html('');
this.prefsDropdown = createElementWithID('div','RESPrefsDropdown','RESDropdownList');
$(this.prefsDropdown).html('
settings console
donate to RES
');
var thisSettingsButton = this.prefsDropdown.querySelector('#SettingsConsole');
this.settingsButton = thisSettingsButton;
thisSettingsButton.addEventListener('click', function() {
RESConsole.hidePrefsDropdown();
RESConsole.open();
}, true);
var thisDonateButton = this.prefsDropdown.querySelector('#RES-donate');
thisDonateButton.addEventListener('click', function() {
RESUtils.openLinkInNewTab('http://redditenhancementsuite.com/contribute.html', true);
}, true);
$(this.prefsDropdown).mouseleave(function() {
RESConsole.hidePrefsDropdown();
});
$(this.prefsDropdown).mouseenter(function() {
clearTimeout(RESConsole.prefsTimer);
});
$(this.gearOverlay).mouseleave(function() {
RESConsole.prefsTimer = setTimeout(function() {
RESConsole.hidePrefsDropdown();
}, 1000);
});
document.body.appendChild(this.gearOverlay);
document.body.appendChild(this.prefsDropdown);
if (RESStorage.getItem('RES.newAnnouncement','true')) {
RESUtils.setNewNotification();
}
},
showPrefsDropdown: function(e) {
var thisTop = parseInt($(RESConsole.userMenu).offset().top + 1, 10);
// var thisRight = parseInt($(window).width() - $(RESConsole.RESPrefsLink).offset().left);
// thisRight = 175-thisRight;
var thisLeft = parseInt($(RESConsole.RESPrefsLink).offset().left - 6, 10);
// $('#RESMainGearOverlay').css('left',thisRight+'px');
$('#RESMainGearOverlay').css('height',$('#header-bottom-right').outerHeight()+'px');
$('#RESMainGearOverlay').css('left',thisLeft+'px');
$('#RESMainGearOverlay').css('top',thisTop+'px');
RESConsole.prefsDropdown.style.top = parseInt(thisTop+$(RESConsole.userMenu).outerHeight(), 10)+'px';
RESConsole.prefsDropdown.style.right = '0px';
RESConsole.prefsDropdown.style.display = 'block';
$('#RESMainGearOverlay').show();
modules['styleTweaks'].setSRStyleToggleVisibility(false, 'prefsDropdown');
},
hidePrefsDropdown: function(e) {
RESConsole.RESPrefsLink.classList.remove('open');
$('#RESMainGearOverlay').hide();
RESConsole.prefsDropdown.style.display = 'none';
modules['styleTweaks'].setSRStyleToggleVisibility(true, 'prefsDropdown');
},
resetModulePrefs: function() {
prefs = {
'userTagger': true,
'betteReddit': true,
'singleClick': true,
'subRedditTagger': true,
'uppersAndDowners': true,
'keyboardNav': true,
'commentPreview': true,
'showImages': true,
'showKarma': true,
'usernameHider': false,
'accountSwitcher': true,
'styleTweaks': true,
'filteReddit': true,
'spamButton': false,
'bitcointip': false,
'RESPro': false
};
this.setModulePrefs(prefs);
return prefs;
},
getAllModulePrefs: function(force) {
var storedPrefs;
// if we've done this before, just return the cached version
if ((!force) && (typeof this.getAllModulePrefsCached !== 'undefined')) return this.getAllModulePrefsCached;
// get the stored preferences out first.
if (RESStorage.getItem('RES.modulePrefs') !== null) {
storedPrefs = safeJSON.parse(RESStorage.getItem('RES.modulePrefs'), 'RES.modulePrefs');
} else if (RESStorage.getItem('modulePrefs') !== null) {
// Clean up old moduleprefs.
storedPrefs = safeJSON.parse(RESStorage.getItem('modulePrefs'), 'modulePrefs');
RESStorage.removeItem('modulePrefs');
this.setModulePrefs(storedPrefs);
} else {
// looks like this is the first time RES has been run - set prefs to defaults...
storedPrefs = this.resetModulePrefs();
}
if (storedPrefs === null) {
storedPrefs = {};
}
// create a new JSON object that we'll use to return all preferences. This is just in case we add a module, and there's no pref stored for it.
var prefs = {};
// for any stored prefs, drop them in our prefs JSON object.
for (var module in modules) {
if (storedPrefs[module]) {
prefs[module] = storedPrefs[module];
} else if ((! modules[module].disabledByDefault) && ((storedPrefs[module] == null) || (module === 'dashboard'))) {
// looks like a new module, or no preferences. We'll default it to on.
// we also default dashboard to on. It's not really supposed to be disabled.
prefs[module] = true;
} else {
prefs[module] = false;
}
}
if ((typeof prefs !== 'undefined') && (prefs !== 'undefined') && (prefs)) {
this.getAllModulePrefsCached = prefs;
return prefs;
}
},
getModulePrefs: function(moduleID) {
if (moduleID) {
var prefs = this.getAllModulePrefs();
return prefs[moduleID];
} else {
alert('no module name specified for getModulePrefs');
}
},
setModulePrefs: function(prefs) {
if (prefs !== null) {
RESStorage.setItem('RES.modulePrefs', JSON.stringify(prefs));
return prefs;
} else {
alert('error - no prefs specified');
}
},
create: function() {
// create the console container
this.RESConsoleContainer = createElementWithID('div', 'RESConsole');
// hide it by default...
// this.RESConsoleContainer.style.display = 'none';
// create a modal overlay
this.modalOverlay = createElementWithID('div', 'modalOverlay');
this.modalOverlay.addEventListener('click', function(e) {
e.preventDefault();
return false;
}, true);
document.body.appendChild(this.modalOverlay);
// create the header
var RESConsoleHeader = createElementWithID('div', 'RESConsoleHeader');
// create the top bar and place it in the header
var RESConsoleTopBar = createElementWithID('div', 'RESConsoleTopBar');
this.logo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADwAAAAeCAMAAABHRo19AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACxFBMVEXw8/wAAAD+//8EBAQSEhIPDw/w8/v+/v4JCQkHBwcCAgKSk5W8vLz9SADz8/MtLS0iIiIcHBz/VAAYGBmRkZFkZGUkJCQVFhZiYmOZmp2QkpfQ09r9/f3n6vA5OTkvLy//TAAxMTEUFRTl5eVqa2zu8fnt7/fV19ydnqCen6Lt8Pj/TwDk5ORaWlrg4ug1NTUpKSrX19cgICDp6/J6enrFxcW1trpDQ0M7OzwnJyenp6f6TQAXFxj/WACFhojr6+uNjpBHR0cfHx+vr7GSkpJMTEwYGBg+Pj5cXF3CwsJISEj29vYQEBDe3t7+SwBmZmixsbH19fXo6OhQUFAgICJgYWXHyM3q7PTs7vW3uLvb3eKqq650dXbS09js7/aTlJY5OjmUlJeenp7r7vWWl5n8/Px4eHihoqWEhYfO0NTj5euDg4Pa3OGRkpTJy8/g4ODe4Obc3Nzv8vqjo6O1tbW3uLyrq6t1dXX5ya5/f3/5xqxZWVqKiopra2v4uJb99vLCw8fFxsouLS6Oj5Hs7OzY2t+jpKZ4eXv2tY8NDQ35WQny8vJkZGT2lWGQkJB8fHzi5OrLzNFAQUPm6O/3f0W7u7v3oXP4dTb2nXH62MX3pHb87+bn5+dWV1dvb3E0NDT4lWP3jFP4vJn2cS79+vaJioxNTU376d72f0H4Wwf2fT7759z9+fX1lmH4XAv2bSb40bheX2A6Ojr9+vj76t/9+vf76+H5XxVGRkZxcnPQ0te+vr52dnaztLfExMT2tZFYWFhSUlLV1dVwcXL52MS4uLiysrKam5rW1tZPT1CVlZWYmJiUlJRHR0ipqq0qKiqzs7P39/fq6urj4+P89fH09PT6+vo4ODjq7PNsbW4oKCh0dHTv7++3t7fk5u2IiYtFRUU3NzdPT0/Kysru7u6NjY1tbW1gYGBfX19sbGyHh4fh4eEzPXfuAAACPElEQVR4Xq3SQ9fkQBTH4bpVSdru17Zt28bYtm3btm3btm37S8yk0oteTKc7c+a3uf/Nc3JyEvT/48KF69Uhu7dk3AfaZ48PRiHgUwLdpGLdtFbecrkPOxvjuSRcmp2vaIsQt6gdLME4UtlGGs6NFW7+GIw7Qidp2BAq3KaQWg650mwC9LSs6JpRfZG03PTo32reMrmzIW3IlGaSZY/W+aCcoY/xq1SCKXAC5xAaGObkFoSmZoK3uaxqlgzL6vol3UohjIpDLWq6J4jaaNZUnsb4syMCsHU5o10q4015sZAshp2LuuCu4DSZFzJrrh0GURj3Ai8BNHrQ08TdyvZXDsDzYBD+W4OJK5bFh9nGIaRuKKTTxw5fOtJTUCtWjh3H31NQiCdOso2DiVlXSsXGDN+M6XRdnlmtmUNXYrGaLPhD3IFvoQfQrH4KkMdRsjgiK2IZXcurs4zHVvFrdSasQTaeTFu7DtPWa4yaDXSd0xh9N22mMyUVieItWwW8bfuOnbvo2r1n7779mOZ6QByHHsRChw4fsXwsz6OPsdDxE0i0kyQA20rLFIhjzuW0TVxIgpB4Z+AsBRXn1RZTdeEivXFyFbLXJTaJvmkDNJgLrly95iR3juTt9eIbyH6ucJPq2hJGQQiru63lbbriDocc6C7cu1/BgwcPH9U/4cdT9TNQIcd6/oK8fFWbg4Vev0n0I6VvkcO9A38Fq495X5T3wZkhLvAROZ6KYT59Lvvy9VvU9x8/1fW/DEygHfEbNdeCkgdk4HMAAAAASUVORK5CYII=';
// this string is split because a specific sequence of characters screws up some git clients into thinking this file is binary.
this.loader = 'data:image/gif;base64,R0lGODlhHQAWANUAAESatESetEyetEyitEyivFSivFSmvFymvFyqvGSqvGSqxGSuxGyuxGyyxHSyxHS2xHS2zHy2zHy6zIS6zIS+zIy+zIzCzIzC1JTG1JzK1JzK3JzO3KTO3KTS3KzS3KzW3LTW3LTW5LTa5Lza5Lze5MTe5MTi5MTi7Mzi7Mzm7NTm7NTq7Nzq7Nzq9Nzu9OTu9OTy9Ozy9Oz29Oz2/PT2/PT6/Pz6/Pz+/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH';
this.loader += '/C05FVFNDQVBFMi4wAwEAAAAh/h1CdWlsdCB3aXRoIEdJRiBNb3ZpZSBHZWFyIDQuMAAh+QQIBgAAACwAAAAAHQAWAAAG/sCbcEgs3myyEIzjQr2MUGjrgpFMrJIMhxTtei4SbPhKwXCeXaLren00GIuHlSLxzNJDD4NOWST8CwsUgxEjeEIcDYN0ICkjFA4UFYMcRXckIS8XKysTCJKSGCMkHBUXpwwXRC8UGheLpgsMDBKmF6YWF7kODYY3LmawoKcXCxIKFMSnkBIELDczIxODk2SmpoMFbg8XDg4SAAoTNTUY1BcTDQsKCw2nGGAMBAUJDQcCDZ8yNzESya8NFDCAEFAChoO6GGSowEDDggsq0HhIZisVixkwQFDBkIHCARQ1XICosSIGEYe5MFjAsE8IigwcYWa402VEyoNmRozgkEFDbs8MBRS0jJJCwAOcMn1u4MBTA4UHNdLIgIAOg08NGphqZWAggohDHBIEqMCRqZYMEjZMMPBgaJcYcDAcQMBhwgMOGOg9AOHrUIkQ8hJQQKDgQaQFEQ4ZuRABxSwREtqWcKHYiIwaWm6UGBG18o0gACH5BAgGAAAALAAAAAAdABYAAAb';
this.loader += '+wJtwSCwKXabWBjaS2YxQowqDkUysEg4GFe1+LtgrVkKddYsvCRbSYCwcEgpl4jGfhR3GnLJILP4JchQQJXdCHhCCEiApIxUNFZESGkUzNCsaMBwjMRQFE3IVGCMkHBYXFBcQGEM1NhRUexWqCRAQsxcWuBcXEQgkQjEXGYIUFanIDxENEry5F48SByo3MCWCx1fGzlcHCxKQEggUAgYWrqjGcg0LCguQuVUNBwUJbgIKDBFmMKi4DfnYKCBDhUqDCRgWYFDmAoYQDs2cMcCwYkaMEBYKUjiAAsaMDzFgxCDiocEpDBcwjBSSIkMGDRkwWHDYJUSqghg2jBjB4eVzSwwKINA4Y0JAhIIuYcLkoKFnAwc1zsyYYCFC0pccsmZNcNCDoQ4FCmAQ1TPr2A4JClCIeufFggcUAkDg8ECCBwkF4F4YYYhlCAQFHEwwwECCAwcINDzpK2QGBQ4gFEwAsSDDDA4vGBOxUaMfFw5cNN8IAgAh+QQIBgAAACwAAAAAHQAWAAAG/sCbcEgsClcqlAc2qtWMUCOKc5FYrZyK6xmFhizWiURMxmBm3SIMMp48GoyFQ0Kpc9BpIcchpiz+';
this.loader += 'gHUUESd5Qh4QghIhKCMUDhQVFBIYRTMvMxgtIxw1GAJ0khkiJRwUF6gRGUNOGRUYghQYEQgSEBcWFBa7uGAEIUI1p7GSFRUXg3MRqKgWFwoRCSs3LiPIkhRkyKgSDggFj3UHEwcEFk8ZoXUNCn8OqBjIDQj0Cg0CCA8PMTctsMcX4jBwwI6SGQsZAnJYcKrBCn43ODxgFvBCixkwvpjJQIGBChU3RqioAVFIiAjOMFjAIGNICgwZNGTA4ABGmhATzZjhMIJTacyYNClwiVLCgKyNP2VyWIqhgIOhUGQkwyBT6VIOGRSA4WCIg4AGHDNgZYrBawEMUKO0aCCBAYALGRiUZVCLwoMRhoS80IDgQIQGBuY0SJDgRMm8MCiguJAgZgIUL23mlcLyBQbJk28EAQAh+QQIBgAAACwAAAAAHQAWAAAG/sCbcEgsClWwEElFstWMUGPpM5FUJxTMBUaLRkcUq2QsplwwXS8R5hBDGoxFm0LXyNRDj4OCXSQWgAl0FBEpeEIce3QSISlgDhUUFRAXRTQqNRwlKhgzGgUQgxkjJRxmFxcTHEMzLyRmgxQaFIIQFReRqBcWFxIDH0MYsZKSu2MMhLoWtwzNKjctHsJ0FWPFqBMLCAIXDxEXBw4MARhPHhKSkXCADbdnFA4KfggNBaASMDecxBcN8g7+JGAYiArEggwOHHRogOLODQ8NdF1YgKHFjCRnBlqQ0MKEjRRN8g0JcWoghhhDUmTIoCEDBQUio3hQYMEkhg0jRnBgyTMLcEovJhbUHLiypQYNOzlIABDhiZcYLx/wbMmh6k4IGbAe0jBgQi+kGapi4FABAAIOP9WsiCDBnksHHDAceEABAgMTh4TMqIBggYQDCCREWHBgAYxneYW0wPCiwQIQEh686FAusREQHmyE4FDDhuUbQQAAIfkECAYAAAAsAAAAAB0AFgAABv7Am3BILN5sqhlHVUrVaMaosSSSUCTYygUTm0mlKKxkIiZTKJrat/hqkCcPhrxhpVQw3rXwA6FMKAoLgoJnVyl6QhwMhRIfKCQUDhV2EBdFNSc0IhwvGiocCH12GSMlHBQXqRIcQzMoKhMWhRQZFwwSERd2uhcWvRQFHkMef4UVkxcVVgtXqRYYWg4HDSs3LRgYs2apvRMGCgJjDxcKoQIYNjcjEWe6DQyBDVpbFg8JDAsGDAcCDxQuN1DwSgVvwYMGCiRgyyYBxQILExR8iBBCzY0QDXz5YoChxQwYIZ5hyAANRokYLkQ8IfJhHoZnMYagyEBTA4QDMNZwMCAS23aGESM6ZNAwlGaFPGByLaRZMwMHDRwaBKCQ7osMCQUk1NQAlYPXlxoUaECE4QCGCKuccqDpwUEABh5eIFoRKUCCqBKIJbgg4V4LREJmPFAQ4UGBRQ0QIJjgggTgISpGmFDwwAODCy0mbHhshIaHQxdG3KhRFXAQACH5BAgGAAAALAAAAAAdABYAAAb+wJtwSCzeaiwYxwVyxWrGqBEVklAkksmFspxJpalHdoydZDu0b7HlME8ejAVDTKFULlC1MAShTCgLCguDC3V+J182QxmFdRIeKSMUDnYUEBhGJy4rGDAeJRwMlHYZI6B3FxcPHUM0ISwVlXUYGA0QWhRbFhe7FhUIHkI1JVaGsbEXERILf6mpuxEDDCs3LncWdRVYuc4WBgsCDxUNFA8CEAUXNzYnVrEUDXEKDXcYFxURB3IICgoCDRhY3EDRLFUDQRAOSqCFAV4KZRgQcMDAYQiJB7xSMcCwggaMEBVoZaAlA0XHEDBqKBLSAZU9DDGGoNCAIYMGBwdiftFQwAJ1Q4ojRnDIYLOoBC9fVORiOFKDTQ0coi44oE7NjAYCKBB1CnVD1JoVDlTUcwEgAy4Zog7lcMDAQhd6qmFIAEBCBgUWODhokKHBgQY648Jg0CCCvwgUEhxIwCFoXCIqXGRIUFOBBxINSDyO4mnGCgoubMDYLCQIACH5BAgGAAAALAAAAAAdABYAAAb+wJtwSCzeaq+W59WZuWrGqFHFkVAkkolFMkrRpFIUZJLFlsmiGLi4gmApjwaD0ZhQ7hfbejhyUOwLCQuDC3d3JWB6QhoIhhEgKCMUfhUVEBlGKCcwFyonHhwOEHcVGCMkHBUXFxUNHEM1HigZFBWGpRENFKsXFr2/FA0hQjAtdoa1uxcSDwyjqr4XfwIKLDcxyYZktau+CgkGDRcPERQBDo1HJ8fSDQsKCw2qGNIQBQsMCQcMAggaLTdQlOPFQIGzBgokYFhIYQGIDA0yFAqR4csNExC6XWBwgcUMGCFKLVwYo0WJGiVW2FB0Q4OWVQtlDJmFQUOGCAlgrOFw4MJ9SAwcRozokEGDhg0cLDiYsWbFlpEZMBQtyoFDBgYOLkABM+NAAQsZpmqoWjUDhwYFPuy5sYwCgppmrVot8EBCBRdrX2AoIADDhAVhGZQ6YEDC1rUrGEwyUIBChAUIFpAwtZaIixkQHEpYUOKqC5aVh7AoYcNDhRozXoQWEgQAIfkECAYAAAAsAAAAAB0AFgAABv7Am3BILN5ostNo5ZmtbMaosZWhUCQTSUVSItWk0hIES5aQJ6UXuLgyZyONBcMhsVIw37VwBJlYFwmACwt2FCNgUEIZCFZZICkjFA4UFRQRG0YuITIaIi0eGBARdhohJRwXqRcLGUQeIRx+dn4SCxWptxYXt1sRIUIuK5V2FZWpEw0OCxYUqbpWBgYsR8NWW3W4FxYOCIMWEg4XAggMFDY1IpW3FHEKCw23GBeSAgoNDAINBQcbLTcqD5rNY6CAAQSCEjAopMAAg4cFGBw0QJFhhpATE1StwrBiRgwQdzBkwEABBo0QNFacKILhgSqFMYak0JAhg4YIEGKC8cDggnZChRxGjOBQk6aGWjLWrKDw4OdIoxqIcnBgwUIeKTEMKFBo0yaHr0Q1GCBwSA9JBwe6fs3AwcKBC+Bc6LkRg0IBBBrmcGDHoYKAtDrnomhwAd8yBggUPAjxoMRcIjFgJJAAYgEEE2NqWHzMpkWNCx5usFDD+UYQACH5BAgGAAAALAAAAAAdABYAAAb+wJtwSCzeajWRqjSKqYxQ6OuCkVgnFMlpVItGR1fJxCrJUkYvb3EliYwfjLijPN501cKQw7zo+ymAEyJqNkIaCYBZICgjFHsVFRIcRjQcMCEbMSESD1gVFBkiJRwWFxQXCxhEIRkeiaeOEgqnFRcVpbUXViBCLSUYr5+fpgsQCqYXyaYUCQQsR8CAn2MUuRcWEgcOC4ALFgcEDBI2NRymtRQNfg25GBMNAQgMDQUJCAUZaS4OFsMMfQ4aKJCAoaAFCBJGLPiEoIQHGEJInFKWqsUMTRQKZrjg4IUNES1klCiCgYGygjGGoMigIUOGahC9bLJQsOCGESM6tGSpYYFwgRlqUgSs6ZKlSw4tQU24EyXGAQgYXGpoqYGDVXMCDozEA+yAggwYrlqV0CBDgwZp8MyQUOABBgMUODiI0MGBgAQhVuAZUqKaAgEQKCBI0CAjA717h9QogaBqggshEnCwkTYxkRU0VkxQYcNETMtBAAAh+QQIBgAAACwAAAAAHQAWAAAG/sCbcEgs3mo0kAuEaq2MUOiLgpFYKZLLaBTthrATSViMrYRe3WILLHk0GAuHhILt1NLDDyNMWSgWCQsLFBNYXHg3HIN0EiApIxQOFBWEHEU1Nh4oKRgvJREMk5MYIyUclBcXCxdEKBcedIUXFAwPCpOpFhSpqQ8Qhy0dHHR0lKgXChIIu7kYWA4DLUcchaJ8vLoUBhELEhYMEg0A4DY1GbMVsw2CCg3pGFUMAgftBgcLBxcyNzEQzBQNFDBwEFACPAwXJjTwEOEBhgQeSMAQIoKChXQXGGBYMSOGiAoHLSxQcePECRsoZhDBoCAVQgwxhqDAoCGDBngqu0A6CI/DdJYONoMaKLCvS4oDDQ5moGlzA4cNSzNEuNNFhoIKFjAE1eCUg9cIARaUQMTBgQAIN716lZr1gIOJeGY0yBehgFaNHBAMYEBiLKIbJDg8KGBgwgMECRxUgNAg5l8hNjQwgAQRw4IUMKQ9JuLiRsUaMEYUfRwEADs=';
RESConsoleTopBar.setAttribute('class','RESDialogTopBar');
$(RESConsoleTopBar).html('
reddit enhancement suite
');
RESConsoleHeader.appendChild(RESConsoleTopBar);
this.RESConsoleVersion = createElementWithID('div','RESConsoleVersion');
$(this.RESConsoleVersion).text('v' + RESVersion);
RESConsoleTopBar.appendChild(this.RESConsoleVersion);
// Create the search bar and place it in the top bar
var RESSearchContainer = modules['settingsNavigation'].renderSearchForm();
RESConsoleTopBar.appendChild(RESSearchContainer);
var RESSubredditLink = createElementWithID('a','RESConsoleSubredditLink');
$(RESSubredditLink).text('/r/Enhancement');
RESSubredditLink.setAttribute('href','http://reddit.com/r/Enhancement');
RESSubredditLink.setAttribute('alt','The RES Subreddit');
RESConsoleTopBar.appendChild(RESSubredditLink);
// create the close button and place it in the header
var RESClose = createElementWithID('span', 'RESClose', 'RESCloseButton');
$(RESClose).text('×');
RESClose.addEventListener('click', function(e) {
e.preventDefault();
RESConsole.close();
}, true);
RESConsoleTopBar.appendChild(RESClose);
this.categories = [];
for (var module in modules) {
if ((typeof modules[module].category !== 'undefined') && (this.categories.indexOf(modules[module].category) === -1)) {
this.categories.push(modules[module].category);
}
}
this.categories.sort();
// create the menu
// var menuItems = this.categories.concat(['RES Pro','About RES'));
var menuItems = this.categories.concat(['About RES']);
var RESMenu = createElementWithID('ul', 'RESMenu');
for (var item = 0; item < menuItems.length; item++) {
var thisMenuItem = document.createElement('li');
$(thisMenuItem).text(menuItems[item]);
thisMenuItem.setAttribute('id', 'Menu-' + menuItems[item]);
thisMenuItem.addEventListener('click', function(e) {
e.preventDefault();
RESConsole.menuClick(this);
}, true);
RESMenu.appendChild(thisMenuItem);
}
RESConsoleHeader.appendChild(RESMenu);
this.RESConsoleContainer.appendChild(RESConsoleHeader);
// Store the menu items in a global variable for easy access by the menu selector function.
RESConsole.RESMenuItems = RESMenu.querySelectorAll('li');
// Create a container for each management panel
this.RESConsoleContent = createElementWithID('div', 'RESConsoleContent');
this.RESConsoleContainer.appendChild(this.RESConsoleContent);
// Okay, the console is done. Add it to the document body.
document.body.appendChild(this.RESConsoleContainer);
window.addEventListener("keydown", function(e) {
if ((RESConsole.captureKey) && (e.keyCode !== 16) && (e.keyCode !== 17) && (e.keyCode !== 18)) {
// capture the key, display something nice for it, and then close the popup...
e.preventDefault();
var keyArray = [e.keyCode, e.altKey, e.ctrlKey, e.shiftKey, e.metaKey];
document.getElementById(RESConsole.captureKeyID).value = keyArray.join(",");
document.getElementById(RESConsole.captureKeyID+'-display').value = RESUtils.niceKeyCode(keyArray);
RESConsole.keyCodeModal.style.display = 'none';
RESConsole.captureKey = false;
}
});
$("#RESConsoleContent").delegate(".keycode + input[type=text][displayonly]", {
focus: function(e) {
var thisXY=RESUtils.getXYpos(this, true);
// show dialog box to grab keycode, but display something nice...
$(RESConsole.keyCodeModal).css({
display: "block",
top: RESUtils.mouseY + "px",
left: RESUtils.mouseX + "px;"
});
// RESConsole.keyCodeModal.style.display = 'block';
RESConsole.captureKey = true;
RESConsole.captureKeyID = this.getAttribute('capturefor');
},
blur: function(e) {
$(RESConsole.keyCodeModal).css("display", "none");
}
});
this.keyCodeModal = createElementWithID('div', 'keyCodeModal');
$(this.keyCodeModal).text('Press a key (or combination with shift, alt and/or ctrl) to assign this action.');
document.body.appendChild(this.keyCodeModal);
},
drawConfigPanel: function(category) {
if (!category) return;
this.drawConfigPanelCategory(category);
},
getModuleIDsByCategory: function(category) {
var moduleList = [];
for (var i in modules) {
if (modules[i].category == category && !modules[i].hidden) moduleList.push(i);
}
moduleList.sort(function(a,b) {
if (modules[a].moduleName.toLowerCase() > modules[b].moduleName.toLowerCase()) return 1;
return -1;
});
return moduleList;
},
drawConfigPanelCategory: function(category, moduleList) {
$(this.RESConsoleConfigPanel).empty();
/*
var moduleTest = RESStorage.getItem('moduleTest');
if (moduleTest) {
console.log(moduleTest);
// TEST loading stored modules...
var evalTest = eval(moduleTest);
}
*/
moduleList = moduleList || this.getModuleIDsByCategory(category);
this.RESConfigPanelModulesPane = createElementWithID('div', 'RESConfigPanelModulesPane');
for (var i=0, len=moduleList.length; iRES Module Configuration Select a module from the column at the left to enable or disable it, and configure its various options.');
this.RESConsoleConfigPanel.appendChild(this.RESConfigPanelOptions);
this.RESConsoleContent.appendChild(this.RESConsoleConfigPanel);
},
updateSelectedModule: function (moduleID) {
var moduleButtons = $(RESConsole.RESConsoleConfigPanel).find('.moduleButton');
moduleButtons.removeClass('active');
moduleButtons.filter(function() { return this.getAttribute('moduleID') === moduleID; })
.addClass('active');
},
drawOptionInput: function(moduleID, optionName, optionObject, isTable) {
var thisOptionFormEle;
switch(optionObject.type) {
case 'textarea':
// textarea...
thisOptionFormEle = createElementWithID('textarea', optionName);
thisOptionFormEle.setAttribute('type','textarea');
thisOptionFormEle.setAttribute('moduleID',moduleID);
$(thisOptionFormEle).html(escapeHTML(optionObject.value));
break;
case 'text':
// text...
thisOptionFormEle = createElementWithID('input', optionName);
thisOptionFormEle.setAttribute('type','text');
thisOptionFormEle.setAttribute('moduleID',moduleID);
thisOptionFormEle.setAttribute('placeHolder',optionObject.placeHolder || '');
thisOptionFormEle.setAttribute('value',optionObject.value);
break;
case 'button':
// button...
thisOptionFormEle = createElementWithID('button', optionName);
thisOptionFormEle.classList.add('RESConsoleButton');
thisOptionFormEle.setAttribute('moduleID',moduleID);
thisOptionFormEle.innerText = optionObject.text;
thisOptionFormEle.addEventListener('click', optionObject.callback, false);
break;
case 'list':
// list...
thisOptionFormEle = createElementWithID('input', optionName);
thisOptionFormEle.setAttribute('class','RESInputList');
thisOptionFormEle.setAttribute('type','text');
thisOptionFormEle.setAttribute('moduleID',moduleID);
// thisOptionFormEle.setAttribute('value',optionObject.value);
existingOptions = optionObject.value;
if (typeof existingOptions === 'undefined') existingOptions = '';
var prepop = [];
var optionArray = existingOptions.split(',');
for (var i=0, len=optionArray.length; i").attr({
id: optionName,
type: "text",
class: "keycode",
moduleID: moduleID
}).css({
border: "1px solid red",
display: "none"
}).val(optionObject.value);
if (isTable) realOptionFormEle.attr('tableOption','true');
var thisKeyCodeDisplay = $("").attr({
id: optionName+"-display",
type: "text",
capturefor: optionName,
displayonly: "true"
}).val(RESUtils.niceKeyCode(optionObject.value));
thisOptionFormEle = $("
").append(realOptionFormEle).append(thisKeyCodeDisplay)[0];
break;
default:
console.log('misconfigured option in module: ' + moduleID);
break;
}
if (isTable) {
thisOptionFormEle.setAttribute('tableOption','true');
}
return thisOptionFormEle;
},
enableModule: function(moduleID, onOrOff) {
var prefs = this.getAllModulePrefs(true);
prefs[moduleID] = !!onOrOff;
this.setModulePrefs(prefs);
},
showConfigOptions: function(moduleID) {
if (!modules[moduleID]) return;
RESConsole.drawConfigOptions(moduleID);
RESConsole.updateSelectedModule(moduleID);
RESConsole.currentModule = moduleID;
RESConsole.RESConsoleContent.scrollTop = 0;
modules['settingsNavigation'].setUrlHash(moduleID);
},
drawConfigOptions: function(moduleID) {
if (modules[moduleID] && modules[moduleID].hidden) return;
var thisOptions = RESUtils.getOptions(moduleID);
var optCount = 0;
this.RESConfigPanelOptions.setAttribute('style','display: block;');
$(this.RESConfigPanelOptions).html('');
// put in the description, and a button to enable/disable the module, first..
var thisHeader = document.createElement('div');
thisHeader.classList.add('moduleHeader');
$(thisHeader).html('' + modules[moduleID].moduleName + '');
var thisToggle = document.createElement('div');
thisToggle.classList.add('moduleToggle');
if (moduleID === 'dashboard') thisToggle.style.display = 'none';
$(thisToggle).html('onoff');
if (modules[moduleID].isEnabled()) thisToggle.classList.add('enabled');
thisToggle.setAttribute('moduleID',moduleID);
thisToggle.addEventListener('click', function(e) {
var activePane = RESConsole.RESConfigPanelModulesPane.querySelector('.active');
var enabled = this.classList.contains('enabled');
if (enabled) {
activePane.classList.remove('enabled');
this.classList.remove('enabled');
RESConsole.moduleOptionsScrim.classList.add('visible');
$('#moduleOptionsSave').hide();
} else {
activePane.classList.add('enabled');
this.classList.add('enabled');
RESConsole.moduleOptionsScrim.classList.remove('visible');
$('#moduleOptionsSave').fadeIn();
}
RESConsole.enableModule(this.getAttribute('moduleID'), !enabled);
}, true);
thisHeader.appendChild(thisToggle);
// not really looping here, just only executing if there's 1 or more options...
for (var i in thisOptions) {
var thisSaveButton = createElementWithID('input','moduleOptionsSave');
thisSaveButton.setAttribute('type','button');
thisSaveButton.setAttribute('value','save options');
thisSaveButton.addEventListener('click', function(e) {
RESConsole.saveCurrentModuleOptions(e);
}, true);
this.RESConsoleConfigPanel.appendChild(thisSaveButton);
var thisSaveStatus = createElementWithID('div','moduleOptionsSaveStatus','saveStatus');
thisHeader.appendChild(thisSaveStatus);
break;
}
var thisDescription = document.createElement('div');
thisDescription.classList.add('moduleDescription');
$(thisDescription).html(modules[moduleID].description);
thisHeader.appendChild(thisDescription);
this.RESConfigPanelOptions.appendChild(thisHeader);
var allOptionsContainer = createElementWithID('div', 'allOptionsContainer');
this.RESConfigPanelOptions.appendChild(allOptionsContainer);
// now draw all the options...
for (var i in thisOptions) {
if (!(thisOptions[i].noconfig)) {
optCount++;
var thisOptionContainer = createElementWithID('div', null, 'optionContainer');
var thisLabel = document.createElement('label');
thisLabel.setAttribute('for',i);
$(thisLabel).text(i);
var thisOptionDescription = createElementWithID('div', null, 'optionDescription');
$(thisOptionDescription).html(thisOptions[i].description);
thisOptionContainer.appendChild(thisLabel);
if (thisOptions[i].type === 'table') {
thisOptionDescription.classList.add('table');
// table - has a list of fields (headers of table), users can add/remove rows...
if (typeof thisOptions[i].fields === 'undefined') {
alert('misconfigured table option in module: ' + moduleID + ' - options of type "table" must have fields defined');
} else {
// get field names...
var fieldNames = [];
// now that we know the field names, get table rows...
var thisTable = document.createElement('table');
thisTable.setAttribute('moduleID',moduleID);
thisTable.setAttribute('optionName',i);
thisTable.setAttribute('class','optionsTable');
var thisThead = document.createElement('thead');
var thisTableHeader = document.createElement('tr'), thisTH;
thisTable.appendChild(thisThead);
for (var j=0;j
---
",
placeholderTemplate: "
---
",
});
})(moduleID, i);
} else {
if ((thisOptions[i].type === 'text') || (thisOptions[i].type === 'password') || (thisOptions[i].type === 'keycode')) thisOptionDescription.classList.add('textInput');
var thisOptionFormEle = this.drawOptionInput(moduleID, i, thisOptions[i]);
thisOptionContainer.appendChild(thisOptionFormEle);
thisOptionContainer.appendChild(thisOptionDescription);
}
var thisClear = document.createElement('div');
thisClear.setAttribute('class','clear');
thisOptionContainer.appendChild(thisClear);
allOptionsContainer.appendChild(thisOptionContainer);
}
}
if (optCount === 0) {
var noOptions = createElementWithID('div','noOptions');
noOptions.classList.add('optionContainer');
$(noOptions).text('There are no configurable options for this module');
this.RESConfigPanelOptions.appendChild(noOptions);
} else {
// var thisSaveStatusBottom = createElementWithID('div','moduleOptionsSaveStatusBottom','saveStatus');
// this.RESConfigPanelOptions.appendChild(thisBottomSaveButton);
// this.RESConfigPanelOptions.appendChild(thisSaveStatusBottom);
this.moduleOptionsScrim = createElementWithID('div','moduleOptionsScrim');
if (modules[moduleID].isEnabled()) {
RESConsole.moduleOptionsScrim.classList.remove('visible');
$('#moduleOptionsSave').fadeIn();
} else {
RESConsole.moduleOptionsScrim.classList.add('visible');
$('#moduleOptionsSave').fadeOut();
}
allOptionsContainer.appendChild(this.moduleOptionsScrim);
// console.log($(thisSaveButton).position());
}
},
deleteOptionRow: function(e) {
var thisRow = e.target.parentNode.parentNode;
$(thisRow).remove();
},
saveCurrentModuleOptions: function(e) {
e.preventDefault();
var panelOptionsDiv = this.RESConfigPanelOptions;
// first, go through inputs that aren't a part of a "table of options"...
var inputs = panelOptionsDiv.querySelectorAll('input, textarea');
for (var i=0, len=inputs.length;ian API that allows you to contribute and include your own modules! \
If you\'ve got bug reports or issues with RES, please see the RESIssues subreddit. If you\'d like to follow progress on RES, or you\'d like to converse with other users, please see the Enhancement subreddit. You can also check the wiki for the FAQ, and more detailed info on each module.
\
If you want to contribute to the RES code base, submit bug reports, or make suggestions, you can also visit RES at Github.
\
If you want to contact me directly with suggestions, bug reports or just want to say you appreciate the work, an email would be great.
\
License: Reddit Enhancement Suite is released under the GPL v3.0.
\
Note: Reddit Enhancement Suite will check, at most once a day, to see if a new version is available. No data about you is sent to me nor is it stored.
\
\
\
About the RES Team
\
Steve Sobel (honestbleeps) is the primary developer of RES. Beyond that, there are a number of people who have contributed code, design and/or great ideas to RES. To read more about the RES team, visit the RES website.
\
\
\
\
';
$(RESConsoleAboutPanel).html(AboutPanelHTML);
var searchPanel = modules['settingsNavigation'].renderSearchPanel();
$('#SearchRES', RESConsoleAboutPanel).append(searchPanel);
$(RESConsoleAboutPanel).find('.moduleButton').click(function(e) {
$('.moduleButton').removeClass('active');
$(this).addClass('active');
var thisID = $(this).attr('id');
var thisPanel = thisID.replace('Button-','');
var visiblePanel = $(this).parent().parent().find('.aboutPanel:visible');
var duration = (e.data && e.data.duration) || $(this).hasClass('active') ? 0 : 400;
$(visiblePanel).fadeOut(duration, function () {
$('#'+thisPanel).fadeIn();
});
});
this.RESConsoleContent.appendChild(RESConsoleAboutPanel);
},
drawProPanel: function() {
RESConsoleProPanel = this.RESConsoleProPanel;
var proPanelHeader = document.createElement('div');
$(proPanelHeader).html('RES Pro allows you to save your preferences to the RES Pro server.
Please note: this is beta functionality right now. Please don\'t consider this to be a "backup" solution just yet. To start, you will need to register for a PRO account first, then email steve@honestbleeps.com with your RES Pro username to get access.');
RESConsoleProPanel.appendChild(proPanelHeader);
this.proSetupButton = createElementWithID('div','RESProSetup');
this.proSetupButton.setAttribute('class','RESButton');
$(this.proSetupButton).text('Configure RES Pro');
this.proSetupButton.addEventListener('click', function(e) {
e.preventDefault();
modules['RESPro'].configure();
}, false);
RESConsoleProPanel.appendChild(this.proSetupButton);
/*
this.proAuthButton = createElementWithID('div','RESProAuth');
this.proAuthButton.setAttribute('class','RESButton');
$(this.proAuthButton).html('Authenticate');
this.proAuthButton.addEventListener('click', function(e) {
e.preventDefault();
modules['RESPro'].authenticate();
}, false);
RESConsoleProPanel.appendChild(this.proAuthButton);
*/
this.proSaveButton = createElementWithID('div','RESProSave');
this.proSaveButton.setAttribute('class','RESButton');
$(this.proSaveButton).text('Save Module Options');
this.proSaveButton.addEventListener('click', function(e) {
e.preventDefault();
// modules['RESPro'].savePrefs();
modules['RESPro'].authenticate(modules['RESPro'].savePrefs());
}, false);
RESConsoleProPanel.appendChild(this.proSaveButton);
/*
this.proUserTaggerSaveButton = createElementWithID('div','RESProSave');
this.proUserTaggerSaveButton.setAttribute('class','RESButton');
$(this.proUserTaggerSaveButton).html('Save user tags to Server');
this.proUserTaggerSaveButton.addEventListener('click', function(e) {
e.preventDefault();
modules['RESPro'].saveModuleData('userTagger');
}, false);
RESConsoleProPanel.appendChild(this.proUserTaggerSaveButton);
*/
this.proSaveCommentsSaveButton = createElementWithID('div','RESProSaveCommentsSave');
this.proSaveCommentsSaveButton.setAttribute('class','RESButton');
$(this.proSaveCommentsSaveButton).text('Save saved comments to Server');
this.proSaveCommentsSaveButton.addEventListener('click', function(e) {
e.preventDefault();
// modules['RESPro'].saveModuleData('saveComments');
modules['RESPro'].authenticate(modules['RESPro'].saveModuleData('saveComments'));
}, false);
RESConsoleProPanel.appendChild(this.proSaveCommentsSaveButton);
this.proSubredditManagerSaveButton = createElementWithID('div','RESProSubredditManagerSave');
this.proSubredditManagerSaveButton.setAttribute('class','RESButton');
$(this.proSubredditManagerSaveButton).text('Save subreddits to server');
this.proSubredditManagerSaveButton.addEventListener('click', function(e) {
e.preventDefault();
// modules['RESPro'].saveModuleData('SubredditManager');
modules['RESPro'].authenticate(modules['RESPro'].saveModuleData('subredditManager'));
}, false);
RESConsoleProPanel.appendChild(this.proSubredditManagerSaveButton);
this.proSaveCommentsGetButton = createElementWithID('div','RESProGetSavedComments');
this.proSaveCommentsGetButton.setAttribute('class','RESButton');
$(this.proSaveCommentsGetButton).text('Get saved comments from Server');
this.proSaveCommentsGetButton.addEventListener('click', function(e) {
e.preventDefault();
// modules['RESPro'].getModuleData('saveComments');
modules['RESPro'].authenticate(modules['RESPro'].getModuleData('saveComments'));
}, false);
RESConsoleProPanel.appendChild(this.proSaveCommentsGetButton);
this.proSubredditManagerGetButton = createElementWithID('div','RESProGetSubredditManager');
this.proSubredditManagerGetButton.setAttribute('class','RESButton');
$(this.proSubredditManagerGetButton).text('Get subreddits from Server');
this.proSubredditManagerGetButton.addEventListener('click', function(e) {
e.preventDefault();
// modules['RESPro'].getModuleData('SubredditManager');
modules['RESPro'].authenticate(modules['RESPro'].getModuleData('subredditManager'));
}, false);
RESConsoleProPanel.appendChild(this.proSubredditManagerGetButton);
this.proGetButton = createElementWithID('div','RESProGet');
this.proGetButton.setAttribute('class','RESButton');
$(this.proGetButton).text('Get options from Server');
this.proGetButton.addEventListener('click', function(e) {
e.preventDefault();
// modules['RESPro'].getPrefs();
modules['RESPro'].authenticate(modules['RESPro'].getPrefs());
}, false);
RESConsoleProPanel.appendChild(this.proGetButton);
this.RESConsoleContent.appendChild(RESConsoleProPanel);
},
open: function(moduleIdOrCategory) {
var category, moduleID;
if (moduleIdOrCategory === 'search') {
moduleID = moduleIdOrCategory;
category = 'About RES';
} else {
var module = modules[moduleIdOrCategory];
moduleID = module && module.moduleID;
category = module && module.category;
}
category = category || moduleIdOrCategory || this.categories[0];
moduleID = moduleID || this.getModuleIDsByCategory(category)[0];
// Draw the config panel
this.drawConfigPanel();
// Draw the about panel
this.drawAboutPanel();
// Draw the RES Pro panel
// this.drawProPanel();
this.openCategoryPanel(category);
this.showConfigOptions(moduleID);
this.isOpen = true;
// hide the ad-frame div in case it's flash, because then it covers up the settings console and makes it impossible to see the save button!
var adFrame = document.getElementById('ad-frame');
if ((typeof adFrame !== 'undefined') && (adFrame !== null)) {
adFrame.style.display = 'none';
}
modules['styleTweaks'].setSRStyleToggleVisibility(false, 'RESConsole');
// var leftCentered = Math.floor((window.innerWidth - 720) / 2);
// modalOverlay.setAttribute('style','display: block; height: ' + document.documentElement.scrollHeight + 'px');
this.modalOverlay.classList.remove('fadeOut');
this.modalOverlay.classList.add('fadeIn');
// this.RESConsoleContainer.setAttribute('style','display: block; left: ' + leftCentered + 'px');
// this.RESConsoleContainer.setAttribute('style','display: block; left: 1.5%;');
this.RESConsoleContainer.classList.remove('slideOut');
this.RESConsoleContainer.classList.add('slideIn');
RESStorage.setItem('RESConsole.hasOpenedConsole', true);
document.body.addEventListener('keyup', RESConsole.handleEscapeKey, false);
},
handleEscapeKey: function(e) {
// don't close if the user is in a token input field (e.g. adding subreddits to a list)
// because they probably just want to cancel the dropdown list
if (e.keyCode === 27 && (document.activeElement.id.indexOf('token-input') === -1)) {
RESConsole.close();
document.body.removeEventListener('keyup', RESConsole.handleEscapeKey, false);
}
},
close: function() {
$('#moduleOptionsSave').fadeOut();
this.isOpen = false;
// Let's be nice to reddit and put their ad frame back now...
var adFrame = document.getElementById('ad-frame');
if ((typeof adFrame !== 'undefined') && (adFrame !== null)) {
adFrame.style.display = 'block';
}
modules['styleTweaks'].setSRStyleToggleVisibility(true, 'RESConsole');
// this.RESConsoleContainer.setAttribute('style','display: none;');
this.modalOverlay.classList.remove('fadeIn');
this.modalOverlay.classList.add('fadeOut');
this.RESConsoleContainer.classList.remove('slideIn');
this.RESConsoleContainer.classList.add('slideOut');
// just in case the user was in the middle of setting a key and decided to close the dialog, clean that up.
if (typeof RESConsole.keyCodeModal !== 'undefined') {
RESConsole.keyCodeModal.style.display = 'none';
RESConsole.captureKey = false;
}
modules['settingsNavigation'].resetUrlHash();
},
menuClick: function(obj) {
if (!obj) return;
var objID = obj.getAttribute('id');
var category = objID.split('-'); category = category[category.length - 1];
var moduleID = this.getModuleIDsByCategory(category)[0];
this.openCategoryPanel(category);
this.showConfigOptions(moduleID);
},
openCategoryPanel: function(category) {
// make all menu items look unselected
$(RESConsole.RESMenuItems).removeClass('active');
// make selected menu item look selected
$(RESConsole.RESMenuItems).filter(function() {
var thisCategory = (this.getAttribute('id') || '').split('-');
thisCategory = thisCategory[thisCategory.length - 1];
if (thisCategory == category) return true;
}).addClass('active');
// hide all console panels
$(RESConsole.RESConsoleContent).find('.RESPanel').hide();
switch(category) {
case 'Menu-About RES': // cruft
case 'About RES':
// show the about panel
$(this.RESConsoleAboutPanel).show();
break;
case 'Menu-RES Pro': // cruft
case 'RES Pro':
// show the pro panel
$(this.RESConsoleProPanel).show();
break;
default:
// show the config panel for the given category
$(this.RESConsoleConfigPanel).show();
this.drawConfigPanelCategory(category);
break;
}
}
};
/************************************************************************************************************
Creating your own module:
Modules must have the following format, with required functions:
- moduleID - the name of the module, i.e. myModule
- moduleName - a "nice name" for your module...
- description - for the config panel, explains what the module is
- isEnabled - should always return RESConsole.getModulePrefs('moduleID') - where moduleID is your module name.
- isMatchURL - should always return RESUtils.isMatchURL('moduleID') - checks your include and exclude URL matches.
- include - an array of regexes to match against location.href (basically like include in GM)
- exclude (optional) - an array of regexes to exclude against location.href
- go - always checks both if isEnabled() and if RESUtils.isMatchURL(), and if so, runs your main code.
modules['myModule'] = {
moduleID: 'myModule',
moduleName: 'my module',
category: 'CategoryName',
options: {
// any configurable options you have go here...
// options must have a type and a value..
// valid types are: text, boolean (if boolean, value must be true or false)
// for example:
defaultMessage: {
type: 'text',
value: 'this is default text',
description: 'explanation of what this option is for'
},
doSpecialStuff: {
type: 'boolean',
value: false,
description: 'explanation of what this option is for'
}
},
description: 'This is my module!',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
include: [
/^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.]+/i,
/^https?:\/\/([a-z]+)\.reddit\.com\/message\/comments\/[-\w\.]+/i
],
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
// do stuff now!
// this is where your code goes...
}
}
}; // note: you NEED this semicolon at the end!
************************************************************************************************************/
modules['subRedditTagger'] = {
moduleID: 'subRedditTagger',
moduleName: 'Subreddit Tagger',
category: 'Filters',
options: {
subReddits: {
type: 'table',
addRowText: '+add tag',
fields: [
{ name: 'subreddit', type: 'text' },
{ name: 'doesntContain', type: 'text' },
{ name: 'tag', type: 'text' }
],
value: [
/*
['somebodymakethis','SMT','[SMT]'],
['pics','pic','[pic]']
*/
],
description: 'Set your subreddits below. For that subreddit, if the title of the post doesn\'t contain what you place in the "doesn\'t contain" field, the subreddit will be tagged with whatever you specify.'
}
},
description: 'Adds tags to posts on subreddits (i.e. [SMT] on SomebodyMakeThis when the user leaves it out)',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
include: [
/^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
],
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
this.checkForOldSettings();
this.SRTDoesntContain = [];
this.SRTTagWith = [];
this.loadSRTRules();
RESUtils.watchForElement('siteTable', modules['subRedditTagger'].scanTitles);
this.scanTitles();
}
},
loadSRTRules: function () {
var subReddits = this.options.subReddits.value;
for (var i=0, len=subReddits.length; i 0) {
RESUtils.setOption('subRedditTagger', 'subReddits', settingsCopy);
}
}
};
modules['uppersAndDowners'] = {
moduleID: 'uppersAndDowners',
moduleName: 'Uppers and Downers Enhanced',
category: 'UI',
options: {
showSigns: {
type: 'boolean',
value: false,
description: 'Show +/- signs next to upvote/downvote tallies.'
},
applyToLinks: {
type: 'boolean',
value: true,
description: 'Uppers and Downers on links.'
},
postUpvoteStyle: {
type: 'text',
value: 'color:rgb(255, 139, 36); font-weight:normal;',
description: 'CSS style for post upvotes'
},
postDownvoteStyle: {
type: 'text',
value: 'color:rgb(148, 148, 255); font-weight:normal;',
description: 'CSS style for post upvotes'
},
commentUpvoteStyle: {
type: 'text',
value: 'color:rgb(255, 139, 36); font-weight:bold;',
description: 'CSS style for comment upvotes'
},
commentDownvoteStyle: {
type: 'text',
value: 'color:rgb(148, 148, 255); font-weight:bold;',
description: 'CSS style for comment upvotes'
},
forceVisible: {
type: 'boolean',
value: false,
description: 'Force upvote/downvote counts to be visible (when subreddit CSS tries to hide them)'
}
},
description: 'Displays up/down vote counts on comments.',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
include: [
/^https?:\/\/([a-z]+)\.reddit\.com\/?(?:\??[\w]+=[\w]+&?)*/i,
/^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\w]+\/?(?:\??[\w]+=[\w]+&?)*$/i,
/^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.]+/i,
/^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/?[-\w\.]*/i,
/^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
],
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
beforeLoad: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
// added code to force inline-block and opacity: 1 to prevent CSS from hiding .res_* classes...
var forceVisible = (this.options.forceVisible.value) ? '; visibility: visible !important; opacity: 1 !important; display: inline-block !important;' : '';
var css = '.res_comment_ups { '+this.options.commentUpvoteStyle.value+forceVisible+' } .res_comment_downs { '+this.options.commentDownvoteStyle.value+forceVisible+' }';
css += '.res_post_ups { '+this.options.postUpvoteStyle.value+forceVisible+' } .res_post_downs { '+this.options.postDownvoteStyle.value+forceVisible+' }';
RESUtils.addCSS(css);
}
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
// get rid of the showTimeStamp options since Reddit now has this feature natively.
if (typeof this.options.showTimestamp !== 'undefined') {
delete this.options.showTimestamp;
RESStorage.setItem('RESoptions.uppersAndDowners', JSON.stringify(modules['uppersAndDowners'].options));
}
if (RESUtils.pageType() === 'comments') {
this.commentsWithMoos = [];
this.moreCommentsIDs = [];
this.applyUppersAndDownersToComments();
RESUtils.watchForElement('newComments', modules['uppersAndDowners'].applyUppersAndDownersToComments);
} else if (RESUtils.pageType() === 'profile') {
this.commentsWithMoos = [];
this.moreCommentsIDs = [];
this.applyUppersAndDownersToMixed();
RESUtils.watchForElement('siteTable', modules['uppersAndDowners'].applyUppersAndDownersToMixed);
} else if ((RESUtils.pageType() === 'linklist') && (this.options.applyToLinks.value)) {
this.linksWithMoos = [];
this.applyUppersAndDownersToLinks();
RESUtils.watchForElement('siteTable', modules['uppersAndDowners'].applyUppersAndDownersToLinks);
}
}
},
applyUppersAndDownersToComments: function(ele) {
if (!ele) {
ele = document.body;
}
if (ele.classList.contains('comment')) {
modules['uppersAndDowners'].showUppersAndDownersOnComment(ele);
} else if (ele.classList.contains('entry')) {
modules['uppersAndDowners'].showUppersAndDownersOnComment(ele.parentNode);
} else {
var allComments = ele.querySelectorAll('div.comment');
RESUtils.forEachChunked(allComments, 15, 1000, function(comment, i, array) {
modules['uppersAndDowners'].showUppersAndDownersOnComment(comment);
});
}
},
applyUppersAndDownersToMixed: function(ele) {
ele = ele || document.body;
var linkList = ele.querySelectorAll('div.thing.link, div.thing.comment'),
displayType = 'regular',
thisPlus, thisMinus;
if (modules['uppersAndDowners'].options.showSigns.value) {
thisPlus = '+';
thisMinus = '-';
} else {
thisPlus = '';
thisMinus = '';
}
for (var i=0, len=linkList.length; i ("+thisPlus+thisups+"|"+thisMinus+thisdowns+") ");
if (displayType === 'regular') {
// thisTagline.insertBefore(upsAndDownsEle, thisTagline.firstChild);
$(thisTagline).prepend(upsAndDownsEle);
} else {
$(thisTagline).after(upsAndDownsEle);
}
}
} else {
modules['uppersAndDowners'].showUppersAndDownersOnComment(linkList[i]);
}
}
},
showUppersAndDownersOnComment: function(commentEle) {
// if this is not a valid comment (e.g. a load more comments div, which has the same classes for some reason)
if ((commentEle.getAttribute('data-votesvisible') === 'true')
|| (commentEle.classList.contains('morechildren'))
|| (commentEle.classList.contains('morerecursion'))
|| (commentEle.classList.contains('score-hidden'))) {
return;
}
commentEle.setAttribute('data-votesvisible', 'true');
var tagline = commentEle.querySelector('p.tagline');
var ups = commentEle.getAttribute('data-ups');
var downs = commentEle.getAttribute('data-downs');
var openparen, closeparen, mooups, moodowns, voteUps, voteDowns, pipe;
var frag = document.createDocumentFragment(); //using a fragment speeds this up by a factor of about 2
if (modules['uppersAndDowners'].options.showSigns.value) {
ups = '+'+ups;
downs = '-'+downs;
}
openparen = document.createTextNode(" (");
frag.appendChild(openparen);
mooups = document.createElement("span");
mooups.className = "res_comment_ups";
voteUps = document.createTextNode(ups);
mooups.appendChild(voteUps);
frag.appendChild(mooups);
pipe = document.createTextNode("|");
tagline.appendChild(pipe);
moodowns = document.createElement("span");
moodowns.className = "res_comment_downs";
voteDowns = document.createTextNode(downs);
moodowns.appendChild(voteDowns);
frag.appendChild(moodowns);
closeparen = document.createTextNode(")");
frag.appendChild(closeparen);
frag.appendChild(openparen);
frag.appendChild(mooups);
frag.appendChild(pipe);
frag.appendChild(moodowns);
frag.appendChild(closeparen);
tagline.appendChild(frag);
},
applyUppersAndDownersToLinks: function(ele) {
// Since we're dealing with max 100 links at a time, we don't need a chunker here...
ele = ele || document.body;
var linkList = ele.querySelectorAll('div.thing.link'),
displayType = 'regular',
thisPlus, thisMinus;
if (modules['uppersAndDowners'].options.showSigns.value) {
thisPlus = '+';
thisMinus = '-';
} else {
thisPlus = '';
thisMinus = '';
}
for (var i=0, len=linkList.length; i ("+thisPlus+thisups+"|"+thisMinus+thisdowns+") ");
if (displayType === 'regular') {
// thisTagline.insertBefore(upsAndDownsEle, thisTagline.firstChild);
$(thisTagline).prepend(upsAndDownsEle);
} else {
$(thisTagline).after(upsAndDownsEle);
}
}
}
}
};
modules['keyboardNav'] = {
moduleID: 'keyboardNav',
moduleName: 'Keyboard Navigation',
category: 'UI',
options: {
// any configurable options you have go here...
// options must have a type and a value..
// valid types are: text, boolean (if boolean, value must be true or false)
// for example:
focusBGColor: {
type: 'text',
value: '#F0F3FC',
description: 'Background color of focused element'
},
focusBorder: {
type: 'text',
value: '',
description: 'border style (e.g. 1px dashed gray) for focused element'
},
focusBGColorNight: {
type: 'text',
value: '#666',
description: 'Background color of focused element in Night Mode'
},
focusFGColorNight: {
type: 'text',
value: '#DDD',
description: 'Foreground color of focused element in Night Mode'
},
focusBorderNight: {
type: 'text',
value: '',
description: 'border style (e.g. 1px dashed gray) for focused element'
},
autoSelectOnScroll: {
type: 'boolean',
value: false,
description: 'Automatically select the topmost element for keyboard navigation on window scroll'
},
scrollOnExpando: {
type: 'boolean',
value: true,
description: 'Scroll window to top of link when expando key is used (to keep pics etc in view)'
},
scrollStyle: {
type: 'enum',
values: [
{ name: 'directional', value: 'directional' },
{ name: 'page up/down', value: 'page' },
{ name: 'lock to top', value: 'top' }
],
value: 'directional',
description: 'When moving up/down with keynav, when and how should RES scroll the window?'
},
commentsLinkNumbers: {
type: 'boolean',
value: true,
description: 'Assign number keys (e.g. [1]) to links within selected comment'
},
commentsLinkNumberPosition: {
type: 'enum',
values: [
{ name: 'Place on right', value: 'right' },
{ name: 'Place on left', value: 'left' }
],
value: 'right',
description: 'Which side commentsLinkNumbers are displayed'
},
commentsLinkNewTab: {
type: 'boolean',
value: true,
description: 'Open number key links in a new tab'
},
clickFocus: {
type: 'boolean',
value: true,
description: 'Move keyboard focus to a link or comment when clicked with the mouse'
},
onHideMoveDown: {
type: 'boolean',
value: true,
description: 'After hiding a link, automatically select the next link'
},
onVoteMoveDown: {
type: 'boolean',
value: false,
description: 'After voting on a link, automatically select the next link'
},
toggleHelp: {
type: 'keycode',
value: [191, false, false, true], // ? (note the true in the shift slot)
description: 'Show help for keyboard shortcuts'
},
toggleCmdLine: {
type: 'keycode',
value: [190, false, false, false], // .
description: 'Show/hide commandline box'
},
hide: {
type: 'keycode',
value: [72, false, false, false], // h
description: 'Hide link'
},
moveUp: {
type: 'keycode',
value: [75, false, false, false], // k
description: 'Move up (previous link or comment)'
},
moveDown: {
type: 'keycode',
value: [74, false, false, false], // j
description: 'Move down (next link or comment)'
},
moveTop: {
type: 'keycode',
value: [75, false, false, true], // shift-k
description: 'Move to top of list (on link pages)'
},
moveBottom: {
type: 'keycode',
value: [74, false, false, true], // shift-j
description: 'Move to bottom of list (on link pages)'
},
moveUpSibling: {
type: 'keycode',
value: [75, false, false, true], // shift-k
description: 'Move to previous sibling (in comments) - skips to previous sibling at the same depth.'
},
moveDownSibling: {
type: 'keycode',
value: [74, false, false, true], // shift-j
description: 'Move to next sibling (in comments) - skips to next sibling at the same depth.'
},
moveUpThread: {
type: 'keycode',
value: [75, true, false, true], // shift-alt-k
description: 'Move to the topmost comment of the previous thread (in comments).'
},
moveDownThread: {
type: 'keycode',
value: [74, true, false, true], // shift-alt-j
description: 'Move to the topmost comment of the next thread (in comments).'
},
moveToTopComment: {
type: 'keycode',
value: [84, false, false, false], // t
description: 'Move to the topmost comment of the current thread (in comments).'
},
moveToParent: {
type: 'keycode',
value: [80, false, false, false], // p
description: 'Move to parent (in comments).'
},
showParents: {
type: 'keycode',
value: [80, false, false, true], // p
description: 'Display parent comments.'
},
followLink: {
type: 'keycode',
value: [13, false, false, false], // enter
description: 'Follow link (hold shift to open it in a new tab) (link pages only)'
},
followLinkNewTab: {
type: 'keycode',
value: [13, false, false, true], // shift-enter
description: 'Follow link in new tab (link pages only)'
},
followLinkNewTabFocus: {
type: 'boolean',
value: true,
description: 'When following a link in new tab - focus the tab?'
},
toggleExpando: {
type: 'keycode',
value: [88, false, false, false], // x
description: 'Toggle expando (image/text/video) (link pages only)'
},
imageSizeUp: {
type: 'keycode',
value: [187, false, false, false],
description: 'Increase the size of image(s) in the highlighted post area'
},
imageSizeDown: {
type: 'keycode',
value: [189, false, false, false],
description: 'Increase the size of image(s) in the highlighted post area'
},
imageSizeUpFine: {
type: 'keycode',
value: [187, false, false, true],
description: 'Increase the size of image(s) in the highlighted post area (finer control)'
},
imageSizeDownFine: {
type: 'keycode',
value: [189, false, false, true],
description: 'Increase the size of image(s) in the highlighted post area (finer control)'
},
previousGalleryImage: {
type: 'keycode',
value: [219, false, false, false], //[
description: 'View the previous image of an inline gallery.'
},
nextGalleryImage: {
type: 'keycode',
value: [221, false, false, false], //]
description: 'View the next image of an inline gallery.'
},
toggleViewImages: {
type: 'keycode',
value: [88, false, false, true], // shift-x
description: 'Toggle "view images" button'
},
toggleChildren: {
type: 'keycode',
value: [13, false, false, false], // enter
description: 'Expand/collapse comments (comments pages only)'
},
followComments: {
type: 'keycode',
value: [67, false, false, false], // c
description: 'View comments for link (shift opens them in a new tab)'
},
followCommentsNewTab: {
type: 'keycode',
value: [67, false, false, true], // shift-c
description: 'View comments for link in a new tab'
},
followLinkAndCommentsNewTab: {
type: 'keycode',
value: [76, false, false, false], // l
description: 'View link and comments in new tabs'
},
followLinkAndCommentsNewTabBG: {
type: 'keycode',
value: [76, false, false, true], // shift-l
description: 'View link and comments in new background tabs'
},
upVote: {
type: 'keycode',
value: [65, false, false, false], // a
description: 'Upvote selected link or comment'
},
downVote: {
type: 'keycode',
value: [90, false, false, false], // z
description: 'Downvote selected link or comment'
},
save: {
type: 'keycode',
value: [83, false, false, false], // s
description: 'Save the current link'
},
reply: {
type: 'keycode',
value: [82, false, false, false], // r
description: 'Reply to current comment (comment pages only)'
},
openBigEditor: {
type: 'keycode',
value: [69, false, true, false], // control-e
description: 'Open the current markdown field in the big editor. (Only when a markdown form is focused)'
},
followSubreddit: {
type: 'keycode',
value: [82, false, false, false], // r
description: 'Go to subreddit of selected link (link pages only)'
},
followSubredditNewTab: {
type: 'keycode',
value: [82, false, false, true], // shift-r
description: 'Go to subreddit of selected link in a new tab (link pages only)'
},
inbox: {
type: 'keycode',
value: [73, false, false, false], // i
description: 'Go to inbox'
},
inboxNewTab: {
type: 'keycode',
value: [73, false, false, true], // shift+i
description: 'Go to inbox in a new tab'
},
profile: {
type: 'keycode',
value: [85, false, false, false], // u
description: 'Go to profile'
},
profileNewTab: {
type: 'keycode',
value: [85, false, false, true], // shift+u
description: 'Go to profile in a new tab'
},
frontPage: {
type: 'keycode',
value: [70, false, false, false], // f
description: 'Go to front page'
},
subredditFrontPage: {
type: 'keycode',
value: [70, false, false, true], // shift-f
description: 'Go to subreddit front page'
},
nextPage: {
type: 'keycode',
value: [78, false, false, false], // n
description: 'Go to next page (link list pages only)'
},
prevPage: {
type: 'keycode',
value: [80, false, false, false], // p
description: 'Go to prev page (link list pages only)'
},
link1: {
type: 'keycode',
value: [49, false, false, false], // 1
description: 'Open first link within comment.',
noconfig: true
},
link2: {
type: 'keycode',
value: [50, false, false, false], // 2
description: 'Open link #2 within comment.',
noconfig: true
},
link3: {
type: 'keycode',
value: [51, false, false, false], // 3
description: 'Open link #3 within comment.',
noconfig: true
},
link4: {
type: 'keycode',
value: [52, false, false, false], // 4
description: 'Open link #4 within comment.',
noconfig: true
},
link5: {
type: 'keycode',
value: [53, false, false, false], // 5
description: 'Open link #5 within comment.',
noconfig: true
},
link6: {
type: 'keycode',
value: [54, false, false, false], // 6
description: 'Open link #6 within comment.',
noconfig: true
},
link7: {
type: 'keycode',
value: [55, false, false, false], // 7
description: 'Open link #7 within comment.',
noconfig: true
},
link8: {
type: 'keycode',
value: [56, false, false, false], // 8
description: 'Open link #8 within comment.',
noconfig: true
},
link9: {
type: 'keycode',
value: [57, false, false, false], // 9
description: 'Open link #9 within comment.',
noconfig: true
},
link10: {
type: 'keycode',
value: [48, false, false, false], // 0
description: 'Open link #10 within comment.',
noconfig: true
}
},
description: 'Keyboard navigation for reddit!',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
include: [
/^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
],
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
beforeLoad: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
var focusFGColorNight, focusBGColor, focusBGColorNight;
if (typeof this.options.focusBGColor === 'undefined') {
focusBGColor = '#F0F3FC';
} else {
focusBGColor = this.options.focusBGColor.value;
}
var borderType = 'outline';
if (BrowserDetect.isOpera()) borderType = 'border';
if (typeof this.options.focusBorder === 'undefined') {
focusBorder = '';
} else {
focusBorder = borderType+': ' + this.options.focusBorder.value + ';';
}
if (!(this.options.focusBGColorNight.value)) {
focusBGColorNight = '#666';
} else {
focusBGColorNight = this.options.focusBGColorNight.value;
}
if (!(this.options.focusFGColorNight.value)) {
focusFGColorNight = '#DDD';
} else {
focusFGColorNight = this.options.focusFGColorNight.value;
}
if (typeof this.options.focusBorderNight === 'undefined') {
focusBorderNight = '';
} else {
focusBorderNight = borderType+': ' + this.options.focusBorderNight.value + ';';
}
// old style: .RES-keyNav-activeElement { '+borderType+': '+focusBorder+'; background-color: '+focusBGColor+'; } \
// this new pure CSS arrow will not work because to position it we must have .RES-keyNav-activeElement position relative, but that screws up image viewer's absolute positioning to
// overlay over the sidebar... yikes.
// .RES-keyNav-activeElement:after { content: ""; float: right; margin-right: -5px; border-color: transparent '+focusBorderColor+' transparent transparent; border-style: solid; border-width: 3px 4px 3px 0; } \
// why !important on .RES-keyNav-activeElement? Because some subreddits are unfortunately using !important for no good reason on .entry divs...
RESUtils.addCSS(' \
.entry { padding-right: 5px; } \
.RES-keyNav-activeElement, .commentarea .RES-keyNav-activeElement .md, .commentarea .RES-keyNav-activeElement.entry .noncollapsed { background-color: '+focusBGColor+' !important; } \
.RES-keyNav-activeElement { '+focusBorder+' } \
.res-nightmode .RES-keyNav-activeElement { '+focusBorderNight+' } \
.res-nightmode .RES-keyNav-activeElement, .res-nightmode .RES-keyNav-activeElement .usertext-body, .res-nightmode .RES-keyNav-activeElement .usertext-body .md, .res-nightmode .RES-keyNav-activeElement .usertext-body .md p, .res-nightmode .commentarea .RES-keyNav-activeElement .noncollapsed, .res-nightmode .RES-keyNav-activeElement .noncollapsed .md, .res-nightmode .RES-keyNav-activeElement .noncollapsed .md p { background-color: '+focusBGColorNight+' !important; color: '+focusFGColorNight+' !important;} \
.res-nightmode .RES-keyNav-activeElement a.title:first-of-type {color: ' + focusFGColorNight + ' !important; } \
#keyHelp { display: none; position: fixed; height: 90%; overflow-y: auto; right: 20px; top: 20px; z-index: 1000; border: 2px solid #aaa; border-radius: 5px; width: 300px; padding: 5px; background-color: #fff; } \
#keyHelp th { font-weight: bold; padding: 2px; border-bottom: 1px dashed #ddd; } \
#keyHelp td { padding: 2px; border-bottom: 1px dashed #ddd; } \
#keyHelp td:first-child { width: 70px; } \
#keyCommandLineWidget { font-size: 14px; display: none; position: fixed; top: 200px; left: 50%; margin-left: -275px; z-index: 100000110; width: 550px; border: 3px solid #555; border-radius: 10px; padding: 10px; background-color: #333; color: #CCC; opacity: 0.95; } \
#keyCommandInput { width: 240px; background-color: #999; margin-right: 10px; } \
#keyCommandInputTip { margin-top: 5px; color: #9F9; } \
#keyCommandInputTip ul { font-size: 11px; list-style-type: disc; } \
#keyCommandInputTip li { margin-left: 15px; } \
#keyCommandInputError { margin-top: 5px; color: red; font-weight: bold; } \
.keyNavAnnotation { font-size: 9px; position: relative; top: -6px; } \
');
}
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
// get rid of antequated option we've removed
this.keyboardNavLastIndexCache = safeJSON.parse(RESStorage.getItem('RESmodules.keyboardNavLastIndex'), false, true);
var idx, now = new Date().getTime();
if (! this.keyboardNavLastIndexCache) {
// this is a one time function to delete old keyboardNavLastIndex junk.
this.keyboardNavLastIndexCache = {};
for (idx in RESStorage) {
if (idx.match(/keyboardNavLastIndex/)) {
var url = idx.replace('RESmodules.keyboardNavLastIndex.','');
this.keyboardNavLastIndexCache[url] = {
index: RESStorage[idx],
updated: now
};
RESStorage.removeItem(idx);
}
}
this.keyboardNavLastIndexCache.lastScan = now;
RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
} else {
// clean cache every 6 hours - delete any urls that haven't been visited in an hour.
if ((typeof this.keyboardNavLastIndexCache.lastScan === 'undefined') || (now - this.keyboardNavLastIndexCache.lastScan > 21600000)) {
for (idx in this.keyboardNavLastIndexCache) {
if ((typeof this.keyboardNavLastIndexCache[idx] === 'object') && (now - this.keyboardNavLastIndexCache[idx].updated > 3600000)) {
delete this.keyboardNavLastIndexCache[idx];
}
}
this.keyboardNavLastIndexCache.lastScan = now;
RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
}
}
if (this.options.autoSelectOnScroll.value) {
window.addEventListener('scroll', modules['keyboardNav'].handleScroll, false);
}
if (typeof this.options.scrollTop !== 'undefined') {
if (this.options.scrollTop.value) this.options.scrollStyle.value = 'top';
delete this.options.scrollTop;
RESStorage.setItem('RESoptions.keyboardNav', JSON.stringify(modules['keyboardNav'].options));
}
this.drawHelp();
this.attachCommandLineWidget();
window.addEventListener('keydown', function(e) {
// console.log(e.keyCode);
modules['keyboardNav'].handleKeyPress(e);
}, true);
this.scanPageForKeyboardLinks();
// listen for new DOM nodes so that modules like autopager, never ending reddit, "load more comments" etc still get keyboard nav.
if (RESUtils.pageType() === 'comments') {
RESUtils.watchForElement('newComments', modules['keyboardNav'].scanPageForNewKeyboardLinks);
} else {
RESUtils.watchForElement('siteTable', modules['keyboardNav'].scanPageForNewKeyboardLinks);
}
}
},
scanPageForNewKeyboardLinks: function() {
modules['keyboardNav'].scanPageForKeyboardLinks(true);
},
setKeyIndex: function() {
var trimLoc = location.href;
// remove any trailing slash from the URL
if (trimLoc.substr(-1) === '/') trimLoc = trimLoc.substr(0,trimLoc.length-1);
if (typeof this.keyboardNavLastIndexCache[trimLoc] === 'undefined') {
this.keyboardNavLastIndexCache[trimLoc] = {};
}
var now = new Date().getTime();
this.keyboardNavLastIndexCache[trimLoc] = {
index: this.activeIndex,
updated: now
};
RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
},
handleScroll: function(e) {
if (modules['keyboardNav'].scrollTimer) clearTimeout(modules['keyboardNav'].scrollTimer);
modules['keyboardNav'].scrollTimer = setTimeout(modules['keyboardNav'].handleScrollAfterTimer, 300);
},
handleScrollAfterTimer: function() {
if ((! modules['keyboardNav'].recentKeyPress) && (! RESUtils.elementInViewport(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]))) {
for (var i=0, len=modules['keyboardNav'].keyboardLinks.length; i 0 ? '/' : '') + command;
this.cmdLineShowTip(str);
} else if (command.slice(0,2) === 'm/') {
str = 'navigate to multi-reddit: /me/' + command;
this.cmdLineShowTip(str);
} else if (command.slice(0,2) === 'u/') {
// get the user name they've typed so far (anything after u/)...
var userString = command.slice(2);
this.cmdLineShowTip('navigate to user profile: ' + userString);
} else if (command.slice(0,1) === '/') {
srString = command.slice(1);
this.cmdLineShowTip('sort by ([n]ew, [t]op, [h]ot, [c]ontroversial): ' + srString);
} else if (command === 'tag') {
if ((typeof this.cmdLineTagUsername === 'undefined') || (this.cmdLineTagUsername === '')) {
var searchArea = modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex];
var authorLink = searchArea.querySelector('a.author');
this.cmdLineTagUsername = authorLink.innerHTML;
}
str = 'tag user ' + this.cmdLineTagUsername;
if (val) {
str += ' as: ' + val;
}
this.cmdLineShowTip(str);
} else if (command === 'user') {
str = 'go to profile';
if (val) {
str += ' for: ' + val;
}
this.cmdLineShowTip(str);
} else if (command === 'sw') {
this.cmdLineShowTip('Switch users to: ' + val);
} else if (command === 'm') {
this.cmdLineShowTip('View messages.');
} else if (command === 'mm') {
this.cmdLineShowTip('View moderator mail.');
} else if (command === 'ls') {
this.cmdLineShowTip('Toggle lightSwitch.');
} else if (command === 'nsfw') {
this.cmdLineShowTip('Toggle nsfw filter on or off');
} else if (command === 'srstyle') {
str = 'toggle subreddit style';
if (val) {
str += ' for: ' + val;
} else {
if (RESUtils.currentSubreddit()) {
str += ' for: ' + RESUtils.currentSubreddit();
}
}
this.cmdLineShowTip(str);
} else if (command === 'search') {
this.cmdLineShowTip('Search RES settings for: ' + val);
} else if (command === 'XHRCache') {
this.cmdLineShowTip('clear - clear the cache (use if inline images aren\'t loading properly)');
} else if (command.slice(0,1) === '?') {
str = 'Currently supported commands:';
str += '
';
str += '
r/[subreddit] - navigates to subreddit
';
str += '
/n, /t, /h or /c - goes to new, top, hot or controversial sort of current subreddit
';
str += '
[number] - navigates to the link with that number (comments pages) or rank (link pages)
';
str += '
tag [text] - tags author of currently selected link/comment as text
';
str += '
sw [username] - switch users to [username]
';
str += '
user [username] or u/[username] - view profile for [username]
';
str += '
u/[username]/m/[multi] - view the multireddit [multi] curated by [username]
';
str += '
m/[multi] - view your multireddit [multi]';
str += '
m - go to inbox
';
str += '
mm - go to moderator mail
';
str += '
ls - toggle lightSwitch
';
str += '
nsfw [on|off] - toggle nsfw filter on/off
';
str += '
srstyle [subreddit] [on|off] - toggle subreddit style on/off (if no subreddit is specified, uses current subreddit)
';
str += '
search [words to search for]- search RES settings
';
str += '
RESStorage [get|set|update|remove] [key] [value] - For debug use only, you shouldn\'t mess with this unless you know what you\'re doing.
';
str += '
XHRCache clear - manipulate the XHR cache
';
str += '
';
this.cmdLineShowTip(str);
} else {
this.cmdLineShowTip('');
}
},
cmdLineShowTip: function(str) {
$(this.commandLineInputTip).html(str);
},
cmdLineShowError: function(str) {
$(this.commandLineInputError).html(str);
},
toggleCmdLine: function(force) {
var open = ((force == null || force) && (this.commandLineWidget.style.display !== 'block'));
delete this.cmdLineTagUsername;
if (open) {
this.cmdLineShowError('');
this.commandLineWidget.style.display = 'block';
setTimeout(function() {
modules['keyboardNav'].commandLineInput.focus();
}, 20);
this.commandLineInput.value = '';
} else {
modules['keyboardNav'].commandLineInput.blur();
this.commandLineWidget.style.display = 'none';
}
modules['styleTweaks'].setSRStyleToggleVisibility(!open, 'cmdline');
},
cmdLineSubmit: function(e) {
e.preventDefault();
$(modules['keyboardNav'].commandLineInputError).html('');
var theInput = modules['keyboardNav'].commandLineInput.value;
// see what kind of input it is:
if (theInput.match('^\/?r/')) {
// subreddit? (r/subreddit or /r/subreddit)
theInput = theInput.replace(/^\/?r\//,'');
location.href = '/r/'+theInput;
} else if (theInput.match('^\/?m/')) {
theInput = theInput.replace(/^\/?m\//,'');
location.href = '/me/m/'+theInput;
} else if (theInput.match('^\/?u/')) {
// subreddit? (r/subreddit or /r/subreddit)
theInput = theInput.replace(/^\/?u\//,'');
location.href = '/u/'+theInput;
} else if (theInput.indexOf('/') === 0) {
// sort...
theInput = theInput.slice(1);
switch (theInput) {
case 'n':
theInput = 'new';
break;
case 't':
theInput = 'top';
break;
case 'h':
theInput = 'hot';
break;
case 'c':
theInput = 'controversial';
break;
}
validSorts = ['new','top','hot','controversial'];
if (validSorts.indexOf(theInput) !== -1) {
if (RESUtils.currentUserProfile()) {
location.href = '/user/'+RESUtils.currentUserProfile()+'?sort='+theInput;
} else if (RESUtils.currentSubreddit()) {
location.href = '/r/'+RESUtils.currentSubreddit()+'/'+theInput;
} else {
location.href = '/'+theInput;
}
} else {
modules['keyboardNav'].cmdLineShowError('invalid sort command - must be [n]ew, [t]op, [h]ot or [c]ontroversial');
return false;
}
} else if (!(isNaN(parseInt(theInput, 10)))) {
if (RESUtils.pageType() === 'comments') {
// comment link number? (integer)
modules['keyboardNav'].commentLink(parseInt(theInput, 10)-1);
} else if (RESUtils.pageType() === 'linklist') {
modules['keyboardNav'].keyUnfocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
modules['keyboardNav'].activeIndex = parseInt(theInput, 10) - 1;
modules['keyboardNav'].keyFocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
modules['keyboardNav'].followLink();
}
} else {
var splitWords = theInput.split(' ');
var command = splitWords[0];
splitWords.splice(0,1);
var val = splitWords.join(' ');
switch (command) {
case 'tag':
var searchArea = modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex];
var tagLink = searchArea.querySelector('a.userTagLink');
if (tagLink) {
RESUtils.click(tagLink);
setTimeout(function() {
if (val !== '') {
document.getElementById('userTaggerTag').value = val;
}
}, 20);
}
break;
case 'sw':
// switch accounts (username is required)
if (val.length <= 1) {
modules['keyboardNav'].cmdLineShowError('No username specified.');
return false;
} else {
// first make sure the account exists...
var accounts = modules['accountSwitcher'].options.accounts.value;
var found = false;
for (var i=0, len=accounts.length; i 2) {
splitWords.splice(0,2);
var value = splitWords.join(' ');
}
// console.log(command);
if (command === 'get') {
alert('Value of RESStorage['+key+']:
');
} else if (command === 'update') {
var now = new Date().getTime();
alert('Value of RESStorage['+key+']:
', function() {
var textArea = document.getElementById('RESStorageUpdate'+now);
if (textArea) {
var value = textArea.value;
RESStorage.setItem(key, value);
}
});
} else if (command === 'remove') {
RESStorage.removeItem(key);
alert('RESStorage['+key+'] deleted');
} else if (command === 'set') {
RESStorage.setItem(key, value);
alert('RESStorage['+key+'] set to:
');
} else {
modules['keyboardNav'].cmdLineShowError('You must specify either "get [key]" or "set [key] [value]"');
}
}
break;
case 'XHRCache':
splitWords = val.split(' ');
if (splitWords.length < 1) {
modules['keyboardNav'].cmdLineShowError('Operation required [clear]');
} else {
switch (splitWords[0]) {
case 'clear':
RESUtils.xhrCache('clear');
break;
default:
modules['keyboardNav'].cmdLineShowError('The only accepted operation is clear');
break;
}
}
break;
case '?':
// user is already looking at help... do nothing.
return false;
break;
default:
modules['keyboardNav'].cmdLineShowError('unknown command - type ? for help');
return false;
break;
}
}
// hide the commandline tool...
modules['keyboardNav'].toggleCmdLine(false);
},
scanPageForKeyboardLinks: function(isNew) {
if (typeof isNew === 'undefined') {
isNew = false;
}
// check if we're on a link listing (regular page, subreddit page, etc) or comments listing...
this.pageType = RESUtils.pageType();
switch(this.pageType) {
case 'linklist':
case 'profile':
// get all links into an array...
var siteTable = document.querySelector('#siteTable');
var stMultiCheck = document.querySelectorAll('#siteTable');
// stupid sponsored links create a second div with ID of sitetable (bad reddit! you should never have 2 IDs with the same name! naughty, naughty reddit!)
if (stMultiCheck.length === 2) {
siteTable = stMultiCheck[1];
}
if (siteTable) {
this.keyboardLinks = document.body.querySelectorAll('div.linklisting .entry');
if (!isNew) {
if ((this.keyboardNavLastIndexCache[location.href]) && (this.keyboardNavLastIndexCache[location.href].index > 0)) {
this.activeIndex = this.keyboardNavLastIndexCache[location.href].index;
} else {
this.activeIndex = 0;
}
if ((this.keyboardNavLastIndexCache[location.href]) && (this.keyboardNavLastIndexCache[location.href].index >= this.keyboardLinks.length)) {
this.activeIndex = 0;
}
}
}
break;
case 'comments':
// get all links into an array...
this.keyboardLinks = document.body.querySelectorAll('#siteTable .entry, div.content > div.commentarea .entry');
if (!(isNew)) {
this.activeIndex = 0;
}
break;
case 'inbox':
var siteTable = document.querySelector('#siteTable');
if (siteTable) {
this.keyboardLinks = siteTable.querySelectorAll('.entry');
this.activeIndex = 0;
}
break;
}
// wire up keyboard links for mouse clicky selecty goodness...
if ((typeof this.keyboardLinks !== 'undefined') && (this.options.clickFocus.value)) {
for (var i=0, len=this.keyboardLinks.length;i span > a');
RESUtils.click(hideLink);
// if ((this.options.onHideMoveDown.value) && (!modules['betteReddit'].options.fixHideLink.value)) {
if (this.options.onHideMoveDown.value) {
this.moveDown();
}
},
followSubreddit: function(newWindow) {
// find the subreddit link and click it...
var srLink = this.keyboardLinks[this.activeIndex].querySelector('A.subreddit');
if (srLink) {
var thisHREF = srLink.getAttribute('href');
if (newWindow) {
var button = (this.options.followLinkNewTabFocus.value) ? 0 : 1,
thisJSON;
if (BrowserDetect.isChrome()) {
thisJSON = {
requestType: 'keyboardNav',
linkURL: thisHREF,
button: button
};
chrome.extension.sendMessage(thisJSON);
} else if (BrowserDetect.isSafari()) {
thisJSON = {
requestType: 'keyboardNav',
linkURL: thisHREF,
button: button
};
safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
} else if (BrowserDetect.isOpera()) {
thisJSON = {
requestType: 'keyboardNav',
linkURL: thisHREF,
button: button
};
opera.extension.postMessage(JSON.stringify(thisJSON));
} else if (BrowserDetect.isFirefox()) {
thisJSON = {
requestType: 'keyboardNav',
linkURL: thisHREF,
button: button
};
self.postMessage(thisJSON);
} else {
window.open(thisHREF);
}
} else {
location.href = thisHREF;
}
}
},
moveUp: function() {
if (this.activeIndex > 0) {
this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
this.activeIndex--;
var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
// skip over hidden elements...
while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex > 0)) {
this.activeIndex--;
thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
}
this.keyFocus(this.keyboardLinks[this.activeIndex]);
if ((!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) || (this.options.scrollStyle.value === 'top')) {
RESUtils.scrollTo(0,thisXY.y);
}
modules['keyboardNav'].recentKey();
}
},
moveDown: function() {
if (this.activeIndex < this.keyboardLinks.length-1) {
this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
this.activeIndex++;
var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
// skip over hidden elements...
while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex < this.keyboardLinks.length-1)) {
this.activeIndex++;
thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
}
this.keyFocus(this.keyboardLinks[this.activeIndex]);
// console.log('xy: ' + RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]).toSource());
/*
if ((!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) || (this.options.scrollTop.value)) {
RESUtils.scrollTo(0,thisXY.y);
}
*/
if (this.options.scrollStyle.value === 'top') {
RESUtils.scrollTo(0,thisXY.y);
} else if ((!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex])))) {
var thisHeight = this.keyboardLinks[this.activeIndex].offsetHeight;
if (this.options.scrollStyle.value === 'page') {
RESUtils.scrollTo(0,thisXY.y);
} else {
RESUtils.scrollTo(0,thisXY.y - window.innerHeight + thisHeight + 5);
}
}
if ((RESUtils.pageType() === 'linklist') && (this.activeIndex == (this.keyboardLinks.length-1) && (modules['neverEndingReddit'].isEnabled() && modules['neverEndingReddit'].options.autoLoad.value))) {
this.nextPage();
}
modules['keyboardNav'].recentKey();
}
},
moveTop: function() {
this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
this.activeIndex = 0;
this.keyFocus(this.keyboardLinks[this.activeIndex]);
var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
RESUtils.scrollTo(0,thisXY.y);
}
modules['keyboardNav'].recentKey();
},
moveBottom: function() {
this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
this.activeIndex = this.keyboardLinks.length-1;
this.keyFocus(this.keyboardLinks[this.activeIndex]);
var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
RESUtils.scrollTo(0,thisXY.y);
}
modules['keyboardNav'].recentKey();
},
moveDownSibling: function() {
if (this.activeIndex < this.keyboardLinks.length-1) {
this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
var thisParent = this.keyboardLinks[this.activeIndex].parentNode;
var childCount = thisParent.querySelectorAll('.entry').length;
this.activeIndex += childCount;
// skip over hidden elements...
var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex < this.keyboardLinks.length-1)) {
this.activeIndex++;
thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
}
if ((this.pageType === 'linklist') || (this.pageType === 'profile')) {
this.setKeyIndex();
}
this.keyFocus(this.keyboardLinks[this.activeIndex]);
if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
RESUtils.scrollTo(0,thisXY.y);
}
}
modules['keyboardNav'].recentKey();
},
moveUpSibling: function() {
if (this.activeIndex < this.keyboardLinks.length-1) {
this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
var thisParent = this.keyboardLinks[this.activeIndex].parentNode,
childCount;
if (thisParent.previousSibling !== null) {
childCount = thisParent.previousSibling.previousSibling.querySelectorAll('.entry').length;
} else {
childCount = 1;
}
this.activeIndex -= childCount;
// skip over hidden elements...
var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex < this.keyboardLinks.length-1)) {
this.activeIndex++;
thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
}
if ((this.pageType === 'linklist') || (this.pageType === 'profile')) {
this.setKeyIndex();
}
this.keyFocus(this.keyboardLinks[this.activeIndex]);
if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
RESUtils.scrollTo(0,thisXY.y);
}
}
modules['keyboardNav'].recentKey();
},
moveUpThread: function() {
if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
this.moveToTopComment();
}
this.moveUpSibling();
},
moveDownThread: function() {
if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
this.moveToTopComment();
}
this.moveDownSibling();
},
moveToTopComment: function() {
if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
var firstParent = this.keyboardLinks[this.activeIndex].parentNode;
//goes up to the root of the current thread
while (!firstParent.parentNode.parentNode.parentNode.classList.contains('content') && (firstParent !== null)) {
this.moveToParent();
firstParent = this.keyboardLinks[this.activeIndex].parentNode;
}
}
},
moveToParent: function() {
if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
var firstParent = this.keyboardLinks[this.activeIndex].parentNode;
// check if we're at the top parent, first... if the great grandparent has a class of content, do nothing.
if (!firstParent.parentNode.parentNode.parentNode.classList.contains('content')) {
if (firstParent !== null) {
this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
var thisParent = firstParent.parentNode.parentNode.previousSibling;
var newKeyIndex = parseInt(thisParent.getAttribute('keyindex'), 10);
this.activeIndex = newKeyIndex;
this.keyFocus(this.keyboardLinks[this.activeIndex]);
var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
RESUtils.scrollTo(0,thisXY.y);
}
}
}
}
modules['keyboardNav'].recentKey();
},
showParents: function() {
if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
var firstParent = this.keyboardLinks[this.activeIndex].parentNode;
if (firstParent != null) {
var button = $(this.keyboardLinks[this.activeIndex]).find('.buttons :not(:first-child) .bylink:first').get(0);
RESUtils.hover.begin(button, {}, modules['showParent'].showCommentHover, {});
}
}
},
toggleChildren: function() {
if (this.activeIndex === 0) {
// Ahh, we're not in a comment, but in the main story... that key should follow the link.
this.followLink();
} else {
// find out if this is a collapsed or uncollapsed view...
var thisCollapsed = this.keyboardLinks[this.activeIndex].querySelector('div.collapsed');
var thisNonCollapsed = this.keyboardLinks[this.activeIndex].querySelector('div.noncollapsed');
if (thisCollapsed.style.display !== 'none') {
thisToggle = thisCollapsed.querySelector('a.expand');
} else {
// check if this is a "show more comments" box, or just contracted content...
moreComments = thisNonCollapsed.querySelector('span.morecomments > a');
if (moreComments) {
thisToggle = moreComments;
} else {
thisToggle = thisNonCollapsed.querySelector('a.expand');
}
// 'continue this thread' links
contThread = thisNonCollapsed.querySelector('span.deepthread > a');
if(contThread){
thisToggle = contThread;
}
}
RESUtils.click(thisToggle);
}
},
toggleExpando: function() {
var thisExpando = this.keyboardLinks[this.activeIndex].querySelector('.expando-button');
if (thisExpando) {
RESUtils.click(thisExpando);
if (this.options.scrollOnExpando.value) {
var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
RESUtils.scrollTo(0,thisXY.y);
}
}
},
imageResize: function(factor) {
var images = $(this.activeElement).find('.RESImage.loaded'),
thisWidth;
for (var i=0, len=images.length; i span > a');
if (saveLink) RESUtils.click(saveLink);
},
saveComment: function() {
var saveComment = this.keyboardLinks[this.activeIndex].querySelector('.saveComments');
if (saveComment) RESUtils.click(saveComment);
},
reply: function() {
// activeIndex = 0 means we're at the original post, not a comment
if ((this.activeIndex > 0) || (RESUtils.pageType() !== 'comments')) {
if ((RESUtils.pageType() === 'comments') && (this.activeIndex === 0) && (! location.href.match('/message/'))) {
$('.usertext-edit textarea:first').focus();
} else {
var commentButtons = this.keyboardLinks[this.activeIndex].querySelectorAll('ul.buttons > li > a');
for (var i=0, len=commentButtons.length;i li > a.comments');
location.href = commentButton.getAttribute('href');
} else {
var firstCommentBox = document.querySelector('.commentarea textarea[name=text]');
firstCommentBox.focus();
}
}
},
navigateTo: function(newWindow,thisHREF) {
if (newWindow) {
var thisJSON;
if (BrowserDetect.isChrome()) {
thisJSON = {
requestType: 'keyboardNav',
linkURL: thisHREF
};
chrome.extension.sendMessage(thisJSON);
} else if (BrowserDetect.isSafari()) {
thisJSON = {
requestType: 'keyboardNav',
linkURL: thisHREF
};
safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
} else if (BrowserDetect.isOpera()) {
thisJSON = {
requestType: 'keyboardNav',
linkURL: thisHREF
};
opera.extension.postMessage(JSON.stringify(thisJSON));
} else {
window.open(thisHREF);
}
} else {
location.href = thisHREF;
}
},
inbox: function(newWindow) {
var thisHREF = location.protocol + '//'+location.hostname+'/message/inbox/';
modules['keyboardNav'].navigateTo(newWindow,thisHREF);
},
profile: function(newWindow) {
var thisHREF = location.protocol + '//'+location.hostname+'/user/'+RESUtils.loggedInUser();
modules['keyboardNav'].navigateTo(newWindow,thisHREF);
},
frontPage: function(subreddit) {
var newhref = location.protocol + '//'+location.hostname+'/';
if (subreddit) {
newhref += 'r/' + RESUtils.currentSubreddit();
}
location.href = newhref;
},
nextPage: function() {
// if Never Ending Reddit is enabled, just scroll to the bottom. Otherwise, click the 'next' link.
if ((modules['neverEndingReddit'].isEnabled()) && (modules['neverEndingReddit'].progressIndicator)) {
RESUtils.click(modules['neverEndingReddit'].progressIndicator);
this.moveBottom();
} else {
// get the first link to the next page of reddit...
var nextPrevLinks = document.body.querySelectorAll('.content .nextprev a');
if (nextPrevLinks.length > 0) {
var nextLink = nextPrevLinks[nextPrevLinks.length-1];
// RESUtils.click(nextLink);
location.href = nextLink.getAttribute('href');
}
}
},
prevPage: function() {
// if Never Ending Reddit is enabled, do nothing. Otherwise, click the 'prev' link.
if (modules['neverEndingReddit'].isEnabled()) {
return false;
} else {
// get the first link to the next page of reddit...
var nextPrevLinks = document.body.querySelectorAll('.content .nextprev a');
if (nextPrevLinks.length > 0) {
var prevLink = nextPrevLinks[0];
// RESUtils.click(prevLink);
location.href = prevLink.getAttribute('href');
}
}
},
getCommentLinks: function(obj) {
if (!obj) obj = this.keyboardLinks[this.activeIndex];
return obj.querySelectorAll('div.md a:not(.expando-button):not(.madeVisible):not([href^="javascript:"])');
},
commentLink: function(num) {
if (this.options.commentsLinkNumbers.value) {
var links = this.getCommentLinks();
if (typeof links[num] !== 'undefined') {
var thisLink = links[num];
if ((thisLink.nextSibling) && (typeof thisLink.nextSibling.tagName !== 'undefined') && (thisLink.nextSibling.classList.contains('expando-button'))) {
thisLink = thisLink.nextSibling;
}
// RESUtils.click(thisLink);
this.handleKeyLink(thisLink);
}
}
}
};
// user tagger functions
modules['userTagger'] = {
moduleID: 'userTagger',
moduleName: 'User Tagger',
category: 'Users',
options: {
/*
defaultMark: {
type: 'text',
value: '_',
description: 'clickable mark for users with no tag'
},
*/
hardIgnore: {
type: 'boolean',
value: false,
description: 'If "hard ignore" is off, only post titles and comment text is hidden. If it is on, the entire block is hidden (or in comments, collapsed).'
},
colorUser: {
type: 'boolean',
value: true,
description: 'Color users based on cumulative upvotes / downvotes'
},
storeSourceLink: {
type: 'boolean',
value: true,
description: 'By default, store a link to the link/comment you tagged a user on'
},
hoverInfo: {
type: 'boolean',
value: true,
description: 'Show information on user (karma, how long they\'ve been a redditor) on hover.'
},
hoverDelay: {
type: 'text',
value: 800,
description: 'Delay, in milliseconds, before hover tooltip loads. Default is 800.'
},
fadeDelay: {
type: 'text',
value: 200,
description: 'Delay, in milliseconds, before hover tooltip fades away. Default is 200.'
},
fadeSpeed: {
type: 'text',
value: 0.3,
description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
},
gildComments: {
type: 'boolean',
value: true,
description: 'When clicking the "give gold" button on the user hover info on a comment, give gold to the comment.'
},
highlightButton: {
type: 'boolean',
value: true,
description: 'Show "highlight" button in user hover info, for distinguishing posts/comments from particular users.'
},
highlightColor: {
type: 'text',
value: '#5544CC',
description: 'Color used to highlight a selected user, when "highlighted" from hover info.'
},
highlightColorHover: {
type: 'text',
value: '#6677AA',
description: 'Color used to highlight a selected user on hover.'
},
USDateFormat: {
type: 'boolean',
value: false,
description: 'Show date (redditor since...) in US format (i.e. 08-31-2010)'
},
vwNumber: {
type: 'boolean',
value: true,
description: 'Show the number (i.e. [+6]) rather than [vw]'
},
vwTooltip: {
type: 'boolean',
value: true,
description: 'Show the vote weight tooltip on hover (i.e. "your votes for...")'
}
},
description: 'Adds a great deal of customization around users - tagging them, ignoring them, and more.',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
include: [
/^https?:\/\/([-\w\.]+\.)?reddit\.com\/[-\w\.]*/i
],
beforeLoad: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
var css = '.comment .tagline { display: inline; }';
css += '#userTaggerToolTip { display: none; position: absolute; width: 334px; height: 248px; }';
css += '#userTaggerToolTip label { margin-top: 5px; clear: both; float: left; width: 110px; }';
css += '#userTaggerToolTip input[type=text], #userTaggerToolTip select { margin-top: 5px; float: left; width: 195px; border: 1px solid #c7c7c7; border-radius: 3px; margin-bottom: 6px; }';
css += '#userTaggerToolTip input[type=checkbox] { margin-top: 5px; float: left; }';
css += '#userTaggerToolTip input[type=submit] { cursor: pointer; position: absolute; right: 16px; bottom: 16px; padding: 3px 5px; font-size: 12px; color: #fff; border: 1px solid #636363; border-radius: 3px; background-color: #5cc410; } ';
css += '#userTaggerToolTip .toggleButton { margin-top: 5px; margin-bottom: 5px; }';
css += '#userTaggerClose { position: absolute; right: 7px; top: 7px; z-index: 11; }';
css += '.ignoredUserComment { color: #CACACA; padding: 3px; font-size: 10px; }';
css += '.ignoredUserPost { color: #CACACA; padding: 3px; font-size: 10px; }';
css += 'a.voteWeight { text-decoration: none; color: #369; }';
css += 'a.voteWeight:hover { text-decoration: none; }';
css += '#authorInfoToolTip { display: none; position: absolute; min-width: 450px; z-index: 10001; }';
css += '#authorInfoToolTip:before { content: ""; position: absolute; top: 10px; left: -26px; border-style: solid; border-width: 10px 29px 10px 0; border-color: transparent #c7c7c7; display: block; width: 0; z-index: 1; }'
css += '#authorInfoToolTip:after { content: ""; position: absolute; top: 10px; left: -24px; border-style: solid; border-width: 10px 29px 10px 0; border-color: transparent #f0f3fc; display: block; width: 0; z-index: 1; }'
css += '#authorInfoToolTip.right:before { content: ""; position: absolute; top: 10px; right: -26px; left: auto; border-style: solid; border-width: 10px 0 10px 29px; border-color: transparent #c7c7c7; display: block; width: 0; z-index: 1; }'
css += '#authorInfoToolTip.right:after { content: ""; position: absolute; top: 10px; right: -24px; left: auto; border-style: solid; border-width: 10px 0 10px 29px; border-color: transparent #f0f3fc; display: block; width: 0; z-index: 1; }'
css += '#authorInfoToolTip .authorFieldPair { clear: both; overflow: auto; margin-bottom: 12px; }';
css += '#authorInfoToolTip .authorLabel { float: left; width: 140px; }';
css += '#authorInfoToolTip .authorDetail { float: left; min-width: 240px; }';
css += '#authorInfoToolTip .blueButton { float: right; margin-left: 8px; margin-top: 12px; }';
css += '#authorInfoToolTip .redButton { float: right; margin-left: 8px; }';
css += '#benefits { width: 200px; margin-left: 0; }';
css += '#userTaggerToolTip #userTaggerVoteWeight { width: 30px; }';
css += '.RESUserTagImage { display: inline-block; width: 16px; height: 8px; background-image: url(\'http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png\'); background-repeat: no-repeat; background-position: -16px -137px; }';
css += '.userTagLink { display: inline-block; }';
css += '.hoverHelp { margin-left: 3px; cursor: pointer; color: #369; text-decoration: underline; }';
css += '.userTagLink.hasTag, #userTaggerPreview { display: inline-block; padding: 0 4px; border: 1px solid #c7c7c7; border-radius: 3px; }';
css += '#userTaggerPreview { float: left; height: 16px; margin-bottom: 10px; }';
css += '#userTaggerToolTip .toggleButton .toggleOn { background-color: #107ac4; color: #fff; }';
css += '#userTaggerToolTip .toggleButton.enabled .toggleOn { background-color: #ddd ; color: #636363; }';
css += '#userTaggerToolTip .toggleButton.enabled .toggleOff { background-color: #d02020; color: #fff; }';
css += '#userTaggerToolTip .toggleButton .toggleOff { background-color: #ddd; color: #636363; } ';
css += '#userTaggerTable th { -moz-user-select: none; -webkit-user-select: none; -o-user-select: none; user-select: none; }'
css += '#userTaggerTable tbody .deleteButton { cursor: pointer; width: 16px; height: 16px; background-image: url(data:image/gif;base64,R0lGODlhEAAQAOZOAP///3F6hcopAJMAAP/M//Hz9OTr8ZqksMTL1P8pAP9MDP9sFP+DIP8zAO7x8/D1/LnEz+vx+Flha+Ln7OLm61hhayk0QCo1QMfR2eDo8b/K1M/U2pqiqcfP15WcpcLK05ymsig0P2lyftnf5naBi8XJzZ6lrJGdqmBqdKissYyZpf/+/puotNzk66ayvtbc4rC7x9Xd5n+KlbG7xpiirnJ+ivDz9KKrtrvH1Ojv9ePq8HF8h2x2gvj9/yYyPmRueFxlb4eRm+71+kFLVdrb3c/X4KOnrYGMl3uGke/0+5Sgq1ZfaY6Xn/X4+f///wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAEAAE4ALAAAAAAQABAAAAexgE6CggGFAYOIiAEPEREPh4lOhpOUgwEAmJmaABuQAUktMUUYGhAwLiwnKp41REYmHB5MQUcyN0iQTjsAHU05ICM4SjMQJIg8AAgFBgcvE5gUJYgiycsHDisCApjagj/VzAACBATa5AJOKOAHAAMMDOTvA05A6w7tC/kL804V9uIKAipA52QJgA82dNAQRyBBgwYJyjmRgKmHkAztHA4YAJHfEB8hLFxI0W4AACcbnQQCADs=)}';
RESUtils.addCSS(css);
}
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
this.usernameRE = /(?:u|user)\/([\w\-]+)/;
// Get user tag data...
var tags = RESStorage.getItem('RESmodules.userTagger.tags');
this.tags = null;
if (typeof tags !== 'undefined') this.tags = safeJSON.parse(tags, 'RESmodules.userTagger.tags', true);
// check if we're using the old method of storing user tags... yuck!
if (this.tags === null) {
this.updateTagStorage();
}
// If we're on the dashboard, add a tab to it...
if (RESUtils.currentSubreddit('dashboard')) {
// add tab to dashboard
modules['dashboard'].addTab('userTaggerContents','My User Tags');
// populate the contents of the tab
var showDiv = $('
Show:
')
var tagFilter = $('')
$(showDiv).append(tagFilter);
$('#userTaggerContents').append(showDiv);
$('#tagFilter').change(function(){
modules['userTagger'].drawUserTagTable();
});
var tagsPerPage = parseInt(modules['dashboard'].options['tagsPerPage'].value, 10);
if (tagsPerPage) {
var controlWrapper = document.createElement('div');
controlWrapper.id = 'tagPageControls';
controlWrapper.className = 'RESGalleryControls';
controlWrapper.page = 1;
controlWrapper.pageCount = 1;
var leftButton = document.createElement("a");
leftButton.className = 'previous noKeyNav';
leftButton.addEventListener('click', function(e){
if (controlWrapper.page === 1) {
controlWrapper.page = controlWrapper.pageCount;
} else {
controlWrapper.page -= 1;
}
modules['userTagger'].drawUserTagTable();
});
controlWrapper.appendChild(leftButton);
var posLabel = document.createElement('span');
posLabel.className = 'RESGalleryLabel';
posLabel.textContent = "1 of 2";
controlWrapper.appendChild(posLabel);
var rightButton = document.createElement("a");
rightButton.className = 'next noKeyNav';
rightButton.addEventListener('click', function(e){
if (controlWrapper.page === controlWrapper.pageCount) {
controlWrapper.page = 1;
} else {
controlWrapper.page += 1;
}
modules['userTagger'].drawUserTagTable();
});
controlWrapper.appendChild(rightButton);
$('#userTaggerContents').append(controlWrapper);
}
var thisTable = $('
');
$(thisTable).append('
Username
Tag
Ignored
Color
Vote Weight
');
$('#userTaggerContents').append(thisTable);
$('#userTaggerTable thead th').click(function(e) {
e.preventDefault();
if ($(this).hasClass('delete')) {
return false;
}
if ($(this).hasClass('active')) {
$(this).toggleClass('descending');
}
$(this).addClass('active');
$(this).siblings().removeClass('active').find('SPAN').remove();
$(this).find('.sortAsc, .sortDesc').remove();
($(e.target).hasClass('descending')) ? $(this).append('') : $(this).append('');
modules['userTagger'].drawUserTagTable($(e.target).attr('sort'), $(e.target).hasClass('descending'));
});
this.drawUserTagTable();
}
// set up an array to cache user data
this.authorInfoCache = [];
if (this.options.colorUser.value) {
this.attachVoteHandlers(document.body);
}
// add tooltip to document body...
this.userTaggerToolTip = createElementWithID('div','userTaggerToolTip', 'RESDialogSmall');
var thisHTML = '
Tag User
';
thisHTML += '
';
$(this.userTaggerToolTip).html(thisHTML);
var ignoreLabel = this.userTaggerToolTip.querySelector('label[for=userTaggerIgnore]');
insertAfter(ignoreLabel, RESUtils.toggleButton('userTaggerIgnore', false, 'no', 'yes'));
this.userTaggerTag = this.userTaggerToolTip.querySelector('#userTaggerTag');
this.userTaggerTag.addEventListener('keyup', modules['userTagger'].updateTagPreview, false);
this.userTaggerColor = this.userTaggerToolTip.querySelector('#userTaggerColor');
this.userTaggerColor.addEventListener('change', modules['userTagger'].updateTagPreview, false);
this.userTaggerPreview = this.userTaggerToolTip.querySelector('#userTaggerPreview');
var userTaggerSave = this.userTaggerToolTip.querySelector('#userTaggerSave');
userTaggerSave.setAttribute('type','submit');
userTaggerSave.setAttribute('value','✓ save tag');
userTaggerSave.addEventListener('click', function(e) {
e.preventDefault();
modules['userTagger'].saveTagForm();
}, false);
var userTaggerClose = this.userTaggerToolTip.querySelector('#userTaggerClose');
userTaggerClose.addEventListener('click', function(e) {
modules['userTagger'].closeUserTagPrompt();
}, false);
//this.userTaggerToolTip.appendChild(userTaggerSave);
this.userTaggerForm = this.userTaggerToolTip.querySelector('FORM');
this.userTaggerForm.addEventListener('submit', function(e) {
e.preventDefault();
modules['userTagger'].saveTagForm();
}, true);
document.body.appendChild(this.userTaggerToolTip);
if (this.options.hoverInfo.value) {
this.authorInfoToolTip = createElementWithID('div', 'authorInfoToolTip', 'RESDialogSmall');
this.authorInfoToolTipHeader = document.createElement('h3');
this.authorInfoToolTip.appendChild(this.authorInfoToolTipHeader);
this.authorInfoToolTipCloseButton = createElementWithID('div', 'authorInfoToolTipClose', 'RESCloseButton');
$(this.authorInfoToolTipCloseButton).text('×');
this.authorInfoToolTip.appendChild(this.authorInfoToolTipCloseButton);
this.authorInfoToolTipCloseButton.addEventListener('click', function(e) {
if (typeof modules['userTagger'].hideTimer !== 'undefined') {
clearTimeout(modules['userTagger'].hideTimer);
}
modules['userTagger'].hideAuthorInfo();
}, false);
this.authorInfoToolTipContents = createElementWithID('div','authorInfoToolTipContents', 'RESDialogContents');
this.authorInfoToolTip.appendChild(this.authorInfoToolTipContents);
this.authorInfoToolTip.addEventListener('mouseover', function(e) {
if (typeof modules['userTagger'].hideTimer !== 'undefined') {
clearTimeout(modules['userTagger'].hideTimer);
}
}, false);
this.authorInfoToolTip.addEventListener('mouseout', function(e) {
if (e.target.getAttribute('class') !== 'hoverAuthor') {
modules['userTagger'].hideTimer = setTimeout(function() {
modules['userTagger'].hideAuthorInfo();
}, modules['userTagger'].options.fadeDelay.value);
}
}, false);
document.body.appendChild(this.authorInfoToolTip);
}
document.getElementById('userTaggerTag').addEventListener('keydown', function(e) {
if (e.keyCode === 27) {
// close prompt.
modules['userTagger'].closeUserTagPrompt();
}
}, true);
//console.log('before applytags: ' + Date());
this.applyTags();
//console.log('after applytags: ' + Date());
if (RESUtils.pageType() === 'comments') {
RESUtils.watchForElement('newComments', modules['userTagger'].attachVoteHandlers);
RESUtils.watchForElement('newComments', modules['userTagger'].applyTags);
} else {
RESUtils.watchForElement('siteTable', modules['userTagger'].attachVoteHandlers);
RESUtils.watchForElement('siteTable', modules['userTagger'].applyTags);
}
var userpagere = /^https?:\/\/([a-z]+).reddit.com\/user\/[-\w\.]+\/?/i;
if (userpagere.test(location.href)) {
var friendButton = document.querySelector('.titlebox .fancy-toggle-button');
if ((typeof friendButton !== 'undefined') && (friendButton !== null)) {
var firstAuthor = document.querySelector('a.author');
if ((typeof firstAuthor !== 'undefined') && (firstAuthor !== null)) {
var thisFriendComment = firstAuthor.getAttribute('title');
thisFriendComment = (thisFriendComment !== null) ? thisFriendComment.substring(8,thisFriendComment.length-1) : '';
} else {
var thisFriendComment = '';
}
// this stopped working. commenting it out for now. if i add this back I need to check if you're reddit gold anyway.
/*
var benefitsForm = document.createElement('div');
var thisUser = document.querySelector('.titlebox > h1').innerHTML;
$(benefitsForm).html('');
insertAfter( friendButton, benefitsForm );
*/
}
}
}
},
attachVoteHandlers: function(obj) {
var voteButtons = obj.querySelectorAll('.arrow');
this.voteStates = [];
for (var i=0, len=voteButtons.length;i tagB) ? 1 : (tagB > tagA) ? -1 : 0;
});
if (this.descending) taggedUsers.reverse();
break;
case 'ignore':
taggedUsers.sort(function(a,b) {
var tagA = (typeof modules['userTagger'].tags[a].ignore === 'undefined') ? 'z' : 'a';
var tagB = (typeof modules['userTagger'].tags[b].ignore === 'undefined') ? 'z' : 'a';
return (tagA > tagB) ? 1 : (tagB > tagA) ? -1 : 0;
});
if (this.descending) taggedUsers.reverse();
break;
case 'color':
taggedUsers.sort(function(a,b) {
var colorA = (typeof modules['userTagger'].tags[a].color === 'undefined') ? 'zzzzz' : modules['userTagger'].tags[a].color.toLowerCase();
var colorB = (typeof modules['userTagger'].tags[b].color === 'undefined') ? 'zzzzz' : modules['userTagger'].tags[b].color.toLowerCase();
return (colorA > colorB) ? 1 : (colorB > colorA) ? -1 : 0;
});
if (this.descending) taggedUsers.reverse();
break;
case 'votes':
taggedUsers.sort(function(a,b) {
var tagA = (typeof modules['userTagger'].tags[a].votes === 'undefined') ? 0 : modules['userTagger'].tags[a].votes;
var tagB = (typeof modules['userTagger'].tags[b].votes === 'undefined') ? 0 : modules['userTagger'].tags[b].votes;
return (tagA > tagB) ? 1 : (tagB > tagA) ? -1 : (a.toLowerCase() > b.toLowerCase());
});
if (this.descending) taggedUsers.reverse();
break;
default:
// sort users, ignoring case
taggedUsers.sort(function(a,b) {
return (a.toLowerCase() > b.toLowerCase()) ? 1 : (b.toLowerCase() > a.toLowerCase()) ? -1 : 0;
});
if (this.descending) taggedUsers.reverse();
break;
}
$('#userTaggerTable tbody').html('');
var tagsPerPage = parseInt(modules['dashboard'].options['tagsPerPage'].value, 10);
var count = taggedUsers.length;
var start = 0;
var end = count;
if (tagsPerPage) {
var tagControls = $('#tagPageControls');
var page = tagControls.prop('page');
var pages = Math.ceil(count / tagsPerPage);
page = Math.min(page, pages);
page = Math.max(page, 1);
tagControls.prop('page', page).prop('pageCount', pages);
tagControls.find('.RESGalleryLabel').text(page + ' of ' + pages);
start = tagsPerPage*(page-1);
end = Math.min(count, tagsPerPage*page);
}
for (var i = start; i < end; i++) {
var thisUser = taggedUsers[i];
var thisTag = (typeof this.tags[thisUser].tag === 'undefined') ? '' : this.tags[thisUser].tag;
var thisVotes = (typeof this.tags[thisUser].votes === 'undefined') ? 0 : this.tags[thisUser].votes;
var thisColor = (typeof this.tags[thisUser].color === 'undefined') ? '' : this.tags[thisUser].color;
var thisIgnore = (typeof this.tags[thisUser].ignore === 'undefined') ? 'no' : 'yes';
var userTagLink = document.createElement('a');
if (thisTag === '') {
// thisTag = '';
userTagLink.setAttribute('class','userTagLink RESUserTagImage');
} else {
userTagLink.setAttribute('class','userTagLink hasTag');
}
$(userTagLink).html(escapeHTML(thisTag));
if (thisColor) {
var bgColor = (thisColor === 'none') ? 'transparent' : thisColor;
userTagLink.setAttribute('style','background-color: '+bgColor+'; color: '+this.bgToTextColorMap[thisColor]+' !important;');
}
userTagLink.setAttribute('username',thisUser);
userTagLink.setAttribute('title','set a tag');
userTagLink.setAttribute('href','javascript:void(0)');
userTagLink.addEventListener('click', function(e) {
modules['userTagger'].openUserTagPrompt(e.target, this.getAttribute('username'));
}, true);
$('#userTaggerTable tbody').append('
'
).show();
} else {
$(event.target).removeClass('expanded');
$(event.target).addClass('collapsedExpando');
$(event.target).addClass('collapsed');
$(event.target).parent().find('.expando').hide();
}
}
},
getSelfTextData: function(href) {
if (!modules['betteReddit'].gettingSelfTextData) {
modules['betteReddit'].gettingSelfTextData = true;
$.getJSON(href, modules['betteReddit'].applyTurboSelfText);
}
},
applyTurboSelfText: function(data) {
var linkList = data.data.children;
delete modules['betteReddit'].gettingSelfTextData;
for (var i=0, len=linkList.length; i 300000) {
GM_xmlhttpRequest({
method: "GET",
url: location.protocol + '//' + location.hostname + "/message/unread/.json?mark=false&app=res",
onload: function(response) {
// save that we've checked in the last 5 minutes
var now = new Date();
RESStorage.setItem('RESmodules.betteReddit.msgCount.lastCheck.'+RESUtils.loggedInUser(), now.getTime());
var data = JSON.parse(response.responseText);
var count = data.data.children.length;
RESStorage.setItem('RESmodules.betteReddit.msgCount.'+RESUtils.loggedInUser(), count);
modules['betteReddit'].setUnreadCount(count);
}
});
} else {
var count = RESStorage.getItem('RESmodules.betteReddit.msgCount.'+RESUtils.loggedInUser());
modules['betteReddit'].setUnreadCount(count);
}
} else {
// console.log('no need to get count - no new mail. resetting lastCheck');
modules['betteReddit'].setUnreadCount(0);
RESStorage.setItem('RESmodules.betteReddit.msgCount.lastCheck.'+RESUtils.loggedInUser(), 0);
}
}
},
setUnreadCount: function(count) {
if (this.options.showUnreadCountInFavicon.value) {
//window.Tinycon.setOptions({ fallback: false });
}
if (count>0) {
if (this.options.showUnreadCountInTitle.value) {
var newTitle = '[' + count + '] ' + document.title.replace(/^\[[\d]+\]\s/,'');
document.title = newTitle;
}
if (this.options.showUnreadCountInFavicon.value) {
//window.Tinycon.setBubble(count);
}
if (this.options.showUnreadCount.value) {
modules['betteReddit'].mailCount.display = 'inline-block'
modules['betteReddit'].mailCount.textContent = '['+count+']';
if (modules['neverEndingReddit'].NREMailCount) {
modules['neverEndingReddit'].NREMailCount.display = 'inline-block'
modules['neverEndingReddit'].NREMailCount.textContent = '['+count+']';
}
}
} else {
var newTitle = document.title.replace(/^\[[\d]+\]\s/,'');
document.title = newTitle;
if (modules['betteReddit'].mailCount) {
modules['betteReddit'].mailCount.display = 'none';
$(modules['betteReddit'].mailCount).html('');
if (modules['neverEndingReddit'].NREMailCount) {
modules['neverEndingReddit'].NREMailCount.display = 'none'
$(modules['neverEndingReddit'].NREMailCount).html('');
}
}
if (this.options.showUnreadCountInFavicon.value) {
//window.Tinycon.setBubble(0);
}
}
},
toolbarFixLinks: [
'etsy.com',
'youtube.com',
'youtu.be',
'twitter.com',
'teamliquid.net',
'flickr.com',
'github.com',
'battle.net',
'play.google.com',
'plus.google.com',
'soundcloud.com'
],
checkToolbarLink: function(url) {
for (var i=0, len=this.toolbarFixLinks.length; i'+ escapeHTML(this.options.fullCommentsText.value) +'');
linkList.appendChild(fullCommentsLink);
}
}
},
editMyComments: function(ele) {
var root = ele || document;
var entries = root.querySelectorAll('#siteTable .entry');
for (var i=0, len=entries.length; iedit');
insertAfter(permalink, editLink);
}
}
},
fixSaveLinks: function(ele) {
var root = ele || document;
var saveLinks = root.querySelectorAll('li:not(.comment-save-button) > FORM.save-button > SPAN > A');
for (var i=0, len=saveLinks.length; i SPAN > A');
for (var i=0, len=saveLinks.length; i SPAN > A');
for (var i=0, len=hideLinks.length; i SPAN > A');
for (var i=0, len=hideLinks.length; i inside the header to replace the subreddit bar (for spacing)
var spacer = document.createElement('div');
// null parameter is necessary for FF3.6 compatibility.
spacer.style.paddingTop = window.getComputedStyle(sb, null).paddingTop;
spacer.style.paddingBottom = window.getComputedStyle(sb, null).paddingBottom;
// HACK: for some reason, if the SM is enabled, the SB gets squeezed horizontally,
// and takes up three rows of vertical space (even at low horizontal resolution).
if (sm) spacer.style.height = (parseInt(window.getComputedStyle(sb, null).height, 10) / 3 - 3)+'px';
else spacer.style.height = window.getComputedStyle(sb, null).height;
//window.setTimeout(function(){
// add the spacer; take the subreddit bar out of the header and put it above
header.insertBefore(spacer, sb);
document.body.insertBefore(sb,header);
// make it fixed
// RESUtils.addCSS('div#sr-header-area {position: fixed; z-index: 10000 !important; left: 0; right: 0; box-shadow: 0 2px 2px #AAA;}');
// something changed on Reddit on 1/31/2012 that made this header-bottom-left margin break subreddit stylesheets... commenting out seems to fix it?
// and now later on 1/31 they've changed it back and I need to add this line back in...
RESUtils.addCSS('#header-bottom-left { margin-top: 19px; }');
RESUtils.addCSS('div#sr-header-area {position: fixed; z-index: 10000 !important; left: 0; right: 0; }');
this.pinCommonElements(sm);
},
pinUserBar: function() {
// Make the user bar at the top of the page a fixed element
this.userBarElement = document.getElementById('header-bottom-right');
var thisHeight = $('#header-bottom-right').height();
RESUtils.addCSS('#header-bottom-right:hover { opacity: 1 !important; }');
RESUtils.addCSS('#header-bottom-right { height: '+parseInt(thisHeight+1, 10)+'px; }');
// make the account switcher menu fixed
window.addEventListener('scroll', modules['betteReddit'].handleScroll, false);
this.pinCommonElements();
},
handleScroll: function(e) {
if (modules['betteReddit'].scrollTimer) clearTimeout(modules['betteReddit'].scrollTimer);
modules['betteReddit'].scrollTimer = setTimeout(modules['betteReddit'].handleScrollAfterTimer, 300);
},
handleScrollAfterTimer: function(e) {
if (RESUtils.elementInViewport(modules['betteReddit'].userBarElement)) {
modules['betteReddit'].userBarElement.setAttribute('style','');
if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
$(modules['accountSwitcher'].accountMenu).attr('style','position: absolute;');
}
} else if (modules['betteReddit'].options.pinHeader.value === 'subanduser') {
if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
$(modules['accountSwitcher'].accountMenu).attr('style','position: fixed;');
}
modules['betteReddit'].userBarElement.setAttribute('style','position: fixed; z-index: 10000 !important; top: 19px; right: 0; opacity: 0.6; -webkit-transition:opacity 0.3s ease-in; -moz-transition:opacity 0.3s ease-in; -o-transition:opacity 0.3s ease-in; -ms-transition:opacity 0.3s ease-in; -transition:opacity 0.3s ease-in;');
} else {
if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
$(modules['accountSwitcher'].accountMenu).attr('style','position: fixed;');
}
modules['betteReddit'].userBarElement.setAttribute('style','position: fixed; z-index: 10000 !important; top: 0; right: 0; opacity: 0.6; -webkit-transition:opacity 0.3s ease-in; -moz-transition:opacity 0.3s ease-in; -o-transition:opacity 0.3s ease-in; -ms-transition:opacity 0.3s ease-in; -transition:opacity 0.3s ease-in;');
}
},
pinHeader: function() {
// Makes the Full header a fixed element
// the subreddit manager code changes the document's structure
var sm = modules['subredditManager'].isEnabled();
var header = document.getElementById('header');
if (header == null) return; // reddit is under heavy load
// add a dummy
to the document for spacing
var spacer = document.createElement('div');
spacer.id = 'RESPinnedHeaderSpacer';
// without the next line, the subreddit manager would make the subreddit bar three lines tall and very narrow
RESUtils.addCSS('#sr-header-area {left: 0; right: 0;}');
spacer.style.height = $('#header').outerHeight() + "px";
// insert the spacer
document.body.insertBefore(spacer, header.nextSibling);
// make the header fixed
RESUtils.addCSS('#header, #RESAccountSwitcherDropdown {position:fixed;}');
// RESUtils.addCSS('#header {left: 0; right: 0; box-shadow: 0 2px 2px #AAA;}');
RESUtils.addCSS('#header {left: 0; right: 0; }');
var headerHeight = $('#header').height() + 15;
RESUtils.addCSS('#RESNotifications { top: '+headerHeight+'px } ');
this.pinCommonElements(sm);
// TODO Needs testing
// Sometimes this gets executed before the subreddit logo has finished loading. When that
// happens, the spacer gets created too short, so when the SR logo finally loads, the header
// grows and overlaps the top of the page, potentially obscuring the first link. This checks
// to see if the image is finished loading. If it is, then the spacer's height is set. Otherwise,
// it pauses, then loops.
// added a check that this element exists, so it doesn't error out RES.
if (document.getElementById('header-img') && (!document.getElementById('header-img').complete)) setTimeout(function(){
if (document.getElementById('header-img').complete)
// null parameter is necessary for FF3.6 compatibility.
document.getElementById('RESPinnedHeaderSpacer').style.height = window.getComputedStyle(document.getElementById('header'), null).height;
else setTimeout(arguments.callee, 10);
}, 10);
},
pinCommonElements: function(sm) {
// pin the elements common to both pinHeader() and pinSubredditBar()
if (sm) {
// RES's subreddit menu
RESUtils.addCSS('#RESSubredditGroupDropdown, #srList, #RESShortcutsAddFormContainer, #editShortcutDialog {position: fixed !important;}');
} else {
RESUtils.addCSS('#sr-more-link: {position: fixed;}');
}
},
restoreSavedTab: function() {
var tabmenu = document.querySelector('#header .tabmenu'),
li = document.createElement('li'),
a = document.createElement('a'),
user = RESUtils.loggedInUser();
a.textContent = 'saved';
a.href = '/user/' + user + '/saved/';
li.appendChild(a);
tabmenu.appendChild(li);
}
};
modules['singleClick'] = {
moduleID: 'singleClick',
moduleName: 'Single Click Opener',
category: 'UI',
options: {
openOrder: {
type: 'enum',
values: [
{ name: 'open comments then link', value: 'commentsfirst' },
{ name: 'open link then comments', value: 'linkfirst' }
],
value: 'commentsfirst',
description: 'What order to open the link/comments in.'
},
hideLEC: {
type: 'boolean',
value: false,
description: 'Hide the [l=c] when the link is the same as the comments page'
},
openBackground: {
type: 'boolean',
value: false,
description: 'Open the [l+c] link in background tabs'
}
},
description: 'Adds an [l+c] link that opens a link and the comments page in new tabs for you in one click.',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
include: [
/^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i,
/^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\._]*\//i
],
exclude: [
/^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\._\/\?]*\/comments[-\w\._\/\?=]*/i
],
beforeLoad: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
RESUtils.addCSS('.redditSingleClick { color: #888; font-weight: bold; cursor: pointer; padding: 0 1px; }');
}
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
// do stuff here!
this.applyLinks();
// listen for new DOM nodes so that modules like autopager, river of reddit, etc still get l+c links...
RESUtils.watchForElement('siteTable', modules['singleClick'].applyLinks);
}
},
applyLinks: function(ele) {
var ele = ele || document;
var entries = ele.querySelectorAll('#siteTable .entry, #siteTable_organic .entry');
for (var i=0, len=entries.length; iSnudown, and therefore SnuOwnd, is a bit funny about how it generates its table of contents entries.
To when it encounters a header it tries to perform some of the usual inline formatting such as emphasis, strikethoughs, or superscript in headers. The text containing generated HTML then gets passed into cb_toc_header which escapes all of the passed HTML. When reddit gets it escaped tags are stripped.
It would be nicer if they just used different functions for rendering the emphasis when making headers.
It seems that my understanding was wrong, for some reason reddit doesn't even use snudown's TOC renderer.
*/
var doc = $('').html(body);
var header_ids = {};
var headers = doc.find('h1, h2, h3, h4, h5, h6');
var tocDiv = $('
').addClass('toc');
var parent = $('
');
parent.data('level', 0);
tocDiv.append(parent);
var level = 0, previous = 0;
var prefix = 'wiki'
headers.each(function(i, e) {
var contents = $(this).text();
var aid = $('
').html(contents).text();
aid = prefix + '_' + aid.replace(/ /g, '_').toLowerCase();
aid = aid.replace(/[^\w.-]/g, function(s) {
return '.' + s.charCodeAt(0).toString(16).toUpperCase();
});
if (!(aid in header_ids)) header_ids[aid] = 0;
var id_num = header_ids[aid] + 1;
header_ids[aid] += 1;
if (id_num > 1) aid = aid + id_num;
$(this).attr('id', aid);
var li = $('
');
});
}
},
viewSource: function(button) {
var buttonList = $(button).parent().parent();
var sourceDiv = $(button).closest('.thing').find(".usertext-edit.viewSource:first");
if (sourceDiv.length !== 0) {
sourceDiv.toggle();
} else {
var permaLink = buttonList.find(".first a");
var jsonURL = permaLink.attr("href");
var urlSplit = jsonURL.split('/');
var postID = urlSplit[urlSplit.length - 1];
var isSelfText = permaLink.is(".comments");
if (jsonURL.indexOf('?context') !== -1) {
jsonURL = jsonURL.replace('?context=3','.json?');
} else {
jsonURL += '/.json';
}
this.gettingSource = this.gettingSource || {};
if (this.gettingSource[postID]) return;
this.gettingSource[postID] = true;
GM_xmlhttpRequest({
method: "GET",
url: jsonURL,
onload: function(response) {
var thisResponse = JSON.parse(response.responseText);
var userTextForm = $('
');
if (!isSelfText) {
var sourceText = null;
if (typeof thisResponse[1] !== 'undefined') {
sourceText = thisResponse[1].data.children[0].data.body;
} else {
var thisData = thisResponse.data.children[0].data;
if (thisData.id == postID) {
sourceText = thisData.body;
} else {
// The message we want is a reply to a PM/modmail, but reddit returns the whole thread.
// So, we have to dig into the replies to find the message we want.
for (var i=0, len=thisData.replies.data.children.length; i limit) {
counter.addClass('tooLong');
} else {
counter.removeClass('tooLong');
}
},
makeEditBar: function() {
if (this.cachedEditBar != null) {
return $(this.cachedEditBar).clone();
}
var editBar = $('
").append(editBar);
if (this.options.commentingAs.value && (!modules['usernameHider'].isEnabled())) {
// show who we're commenting as...
var commentingAs = $('
').text('Commenting as: ' + RESUtils.loggedInUser());
wrappedEditBar.append(commentingAs);
}
this.cachedEditBar = wrappedEditBar;
return this.cachedEditBar;
},
macroDropDownTable: {},
getMacroGroup: function(groupName) {
//Normalize and supply a default group name{}
groupName = (groupName||"").toString().trim() || "macros";
var macroGroup;
if (groupName in this.macroDropDownTable) {
macroGroup = this.macroDropDownTable[groupName];
} else {
macroGroup = this.macroDropDownTable[groupName] = {};
macroGroup.titleButton = $(''+groupName+'');
macroGroup.container = $(''+groupName+'').hide();
macroGroup.dropdown = $('
');
macroGroup.container.append(macroGroup.dropdown);
}
return macroGroup;
},
addButtonToMacroGroup: function(groupName, button) {
var group = this.getMacroGroup(groupName);
group.dropdown.append($("
');
this.autoCompletePop.delegate(".choice", "click mousedown", function(e) {
modules["commentTools"].autoCompleteHideDropdown();
modules["commentTools"].autoCompleteInsert(this.innerHTML);
});
$("body").append(this.autoCompletePop);
$("body").delegate(".usertext .usertext-edit textarea, #BigText, #wiki_page_content", {
keyup: this.autoCompleteTrigger,
keydown: this.autoCompleteNavigate,
blur: this.autoCompleteHideDropdown
});
},
autoCompleteLastTarget: null,
autoCompleteTrigger: function(e) {
var mod = modules["commentTools"];
var KEYS = mod.KEYS;
//\0x08 is backspace
if (/[^A-Za-z0-9 \x08]/.test(String.fromCharCode(e.keyCode))) return true;
mod.autoCompleteLastTarget = this;
var matchRE = /\W\/?([ru])\/([\w\.]*)$/;
var matchSkipRE = /\W\/?([ru])\/([\w\.]*)\ $/;
var fullText = $(this).val();
var prefixText = fullText.slice(0, this.selectionStart);
var match = matchRE.exec(" " + prefixText);
if (match != null) {
if (match[1] === "r" && mod.options.subredditAutocomplete.value == false) return;
if (match[1] === "u" && mod.options.userAutocomplete.value == false) return;
}
if(match==null || match[2] === "" || match[2].length > 10) {
if (e.keyCode === KEYS.SPACE || e.keyCode === KEYS.ENTER) {
var match = matchSkipRE.exec(" " + prefixText);
if (match) {
mod.autoCompleteInsert(match[2]);
}
}
return mod.autoCompleteHideDropdown();
}
var type = match[1];
var query = match[2].toLowerCase();
var queryId = type+"/"+query;
var cache = mod.autoCompleteCache;
if (queryId in cache) {
return mod.autoCompleteUpdateDropdown(cache[queryId]);
}
RESUtils.debounce("autoComplete", 300, function() {
if (type === "r") {
mod.getSubredditCompletions(query);
} else if (type === "u") {
mod.getUserCompletions(query);
}
});
},
getSubredditCompletions: function(query) {
var mod = modules['commentTools'];
if (this.options.subredditAutocomplete.value) {
$.ajax({
type: "POST",
url: "/api/search_reddit_names.json",
data: {query: query, app: "res"},
dataType: "json",
success: function(data) {
mod.autoCompleteCache['r/'+query] = data.names;
mod.autoCompleteUpdateDropdown(data.names);
mod.autoCompleteSetNavIndex(0);
}
});
}
},
getUserCompletions: function(query) {
if (this.options.userAutocomplete.value) {
var tags = JSON.parse(RESStorage.getItem("RESmodules.userTagger.tags"));
var tagNames = Object.keys(tags);
var pageNames = [].map.call($(".author"), function(e) {
return e.innerHTML;
});
var names = tagNames.concat(pageNames);
names = names.filter(function(e, i, a) {
return e.toLowerCase().indexOf(query) === 0;
}).sort().reduce(function(prev, current, i, a) {
//Removing duplicates
if (prev[prev.length - 1] != current) {
prev.push(current);
}
return prev;
}, []);
this.autoCompleteCache['u/'+query] = names;
this.autoCompleteUpdateDropdown(names);
this.autoCompleteSetNavIndex(0);
}
},
autoCompleteNavigate: function(e) {
//Don't mess with shortcuts for fancier cursor movement
if (e.metaKey || e.shiftKey || e.ctrlKey || e.altKey) return;
var mod = modules["commentTools"];
var KEYS = mod.KEYS;
var entries = mod.autoCompletePop.find("a.choice");
var index = +mod.autoCompletePop.find(".selectedItem").attr("data-index");
if (mod.autoCompletePop.is(':visible')) {
switch (e.keyCode) {
case KEYS.DOWN:
case KEYS.RIGHT:
e.preventDefault();
if (index < entries.length-1) index++;
mod.autoCompleteSetNavIndex(index);
break;
case KEYS.UP:
case KEYS.LEFT:
e.preventDefault();
if (index > 0) index--;
mod.autoCompleteSetNavIndex(index);
break;
case KEYS.TAB:
case KEYS.ENTER:
e.preventDefault();
$(entries[index]).click();
break;
case KEYS.ESCAPE:
e.preventDefault();
mod.autoCompleteHideDropdown();
return false;
break;
}
}
},
autoCompleteSetNavIndex: function(index) {
var entries = modules["commentTools"].autoCompletePop.find("a.choice");
entries.removeClass("selectedItem");
entries.eq(index).addClass("selectedItem");
},
autoCompleteHideDropdown: function() {
modules["commentTools"].autoCompletePop.hide();
},
autoCompleteUpdateDropdown: function(names) {
var mod = modules["commentTools"];
if(!names.length) return mod.autoCompleteHideDropdown();
mod.autoCompletePop.empty();
$.each(names.slice(0, 20), function(i, e) {
mod.autoCompletePop.append(''+e+'');
});
var textareaOffset = $(mod.autoCompleteLastTarget).offset();
textareaOffset.left += $(mod.autoCompleteLastTarget).width();
mod.autoCompletePop.css(textareaOffset).show();
mod.autoCompleteSetNavIndex(0);
},
autoCompleteInsert: function(inputValue) {
var textarea = modules["commentTools"].autoCompleteLastTarget,
caretPos = textarea.selectionStart,
left = textarea.value.substr(0, caretPos),
right = textarea.value.substr(caretPos);
left = left.replace(/\/?([ru])\/(\w*)\ ?$/, '/$1/'+inputValue+' ');
textarea.value = left + right;
textarea.selectionStart = textarea.selectionEnd = left.length;
textarea.focus()
},
findTextareaForElement: function(elem) {
return $(elem)
.closest(".usertext-edit, .RESDialogContents, .wiki-page-content")
.find("textarea")
.filter("#BigText, [name=text], [name=description], [name=public_description], #wiki_page_content")
.first();
}
};
modules['usernameHider'] = {
moduleID: 'usernameHider',
moduleName: 'Username Hider',
category: 'Accounts',
options: {
displayText: {
type: 'text',
value: '~anonymous~',
description: 'What to replace your username with, default is ~anonymous~'
}
},
description: 'This module hides your real username when you\'re logged in to reddit.',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
include: [
/^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i,
/^https?:\/\/reddit\.com\/[-\w\.\/]*/i
],
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
beforeLoad: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
if (!RESUtils.loggedInUser(true)) {
this.tryAgain = true;
return false;
}
this.hideUsername();
}
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
if (this.tryAgain && RESUtils.loggedInUser()) {
this.hideUsername();
GM_addStyle(RESUtils.css);
}
}
},
hideUsername: function() {
var user = RESUtils.loggedInUser(),
curatedBy = document.querySelector('.multi-details > h2 a');
RESUtils.addCSS('p.tagline > a[href*=\'/'+user+'\'], #header .user > a, .titlebox .tagline a.author, .commentingAs, .bottom a[href*=\'/'+user+'\'] {line-height:0;font-size:0;content:none;}');
RESUtils.addCSS('p.tagline > a[href*=\'/'+user+'\']:after, #header .user > a:after, .titlebox .tagline a.author:after, .bottom a[href*=\'/'+user+'\']:after {content: "'+this.options.displayText.value+'";letter-spacing:normal;font-size:10px;}');
RESUtils.addCSS('.commentingAs:after {content: "Commenting as: '+this.options.displayText.value+'";letter-spacing:normal;font-size:12px;}');
if ( modules['userHighlight'].isEnabled() ){
RESUtils.addCSS('p.tagline > .submitter[href*=\'/'+user+'\']:after, p.tagline > .moderator[href*=\'/'+user+'\']:after{background-color:inherit;padding:0 2px;font-weight:bold;border-radius:3px;color:#fff;}');
RESUtils.addCSS('p.tagline > .submitter[href*=\'/'+user+'\']:after{ background-color:'+modules['userHighlight'].options.OPColor.value+';}');
RESUtils.addCSS('p.tagline > .moderator[href*=\'/'+user+'\']:after{ background-color:'+modules['userHighlight'].options.modColor.value+';}');
RESUtils.addCSS('p.tagline > .submitter[href*=\'/'+user+'\']:hover:after{ background-color:'+modules['userHighlight'].options.OPColorHover.value+';}');
RESUtils.addCSS('p.tagline > .moderator[href*=\'/'+user+'\']:hover:after{ background-color:'+modules['userHighlight'].options.modColorHover.value+';}');
}
if ( curatedBy && curatedBy.href.slice(-(user.length+1)) === '/' + user ){
curatedBy.textContent = 'curated by /u/' + this.options.displayText.value;
}
}
};
/* siteModule format:
name: {
//Initialization method for things that cannot be performed inline. The method
//is required to be present, but it can be empty
go: function(){},
//Returns true/false to indicate whether the siteModule will attempt to handle the link
//the only parameter is the anchor element
//returns true or false
detect: function(element) {return true/false;},
//This is where links are parsed, cache checks are made, and XHR is performed.
//the only parameter is the anchor element
//The method is in a jQuery Deferred chain and will be followed by handleInfo.
//A new $.Deferred object should be created and resolved/rejected as necessary and then reterned.
//If resolving, the element should be passed along with whatever data is required.
handleLink: function(element) {},
//This is were the embedding information is added to the link
//handleInfo sits in the Deferred chain after handLink
//and should recieve both the element and a data object from handleLink
//the first parameter should the same anchor element passed to handleLink
//the second parameter should module specific data
//A new $.Deferred object should be created and resolved/rejected as necessary and then reterned
//If resolving, the element should be passed
handleInfo: function(elem, info) {}
*/
/*
Embedding infomation:
all embedding information (except 'site') is to be attatched the
html anchor in the handleInfo function
required type:
'IMAGE' for single images | 'GALLERY' for image galleries | 'TEXT' html/text to be displayed
required src:
if type is TEXT then src is HTML (be carefull what is accepted here)
if type is IMAGE then src is an image URL string
if type is GALLERY then src is an array of objects with the following properties:
required src: URL of the image
optional href: URL of the page containing the image (per image)
optional title: string to displayed directly above the image (per image)
optional caption: string to be displayed directly below the image (per image)
optional imageTitle:
string to be displayed above the image (gallery level).
optional caption:
string to be displayed below the image
optional credits:
string to be displayed below caption
optional galleryStart:
zero-indexed page number to open the gallery to
*/
modules['showImages'] = {
moduleID: 'showImages',
moduleName: 'Inline Image Viewer',
category: 'UI',
options: {
maxWidth: {
type: 'text',
value: '640',
description: 'Max width of image displayed onscreen'
},
maxHeight: {
type: 'text',
value: '480',
description: 'Max height of image displayed onscreen'
},
openInNewWindow: {
type: 'boolean',
value: true,
description: 'Open images in a new tab/window when clicked?'
},
hideNSFW: {
type: 'boolean',
value: false,
description: 'If checked, do not show images marked NSFW.'
},
autoExpandSelfText: {
type: 'boolean',
value: true,
description: 'When loading selftext from an Aa+ expando, auto reveal images.'
},
imageZoom: {
type: 'boolean',
value: true,
description: 'Allow dragging to resize/zoom images.'
},
markVisited: {
type: 'boolean',
value: true,
description: 'Mark links visited when you view images (does eat some resources).'
},
sfwHistory: {
type: 'enum',
value: 'add',
values: [
{name: 'Add links to history', value: 'add'},
{name: 'Color links, but do not add to history', value: 'color'},
{name: 'Do not add or color links.', value: 'none'}
],
description: 'Keeps NSFW links from being added to your browser history by the markVisited feature. \
If you chose the second option, then links will be blue again on refresh. \
This does not change your basic browser behavior.\
If you click on a link then it will still be added to your history normally.\
This is not a substitute for using your browser\'s privacy mode.'
},
ignoreDuplicates: {
type: 'boolean',
value: true,
description: 'Do not create expandos for images that appear multiple times in a page.'
},
displayImageCaptions: {
type: 'boolean',
value: true,
description: 'Retrieve image captions/attribution information.'
},
loadAllInAlbum: {
type: 'boolean',
value: false,
description: 'Display all images at once in a \'filmstrip\' layout, rather than the default navigable \'slideshow\' style.'
}
},
description: 'Opens images inline in your browser with the click of a button. Also has configuration options, check it out!',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
include: [
/^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\_\?=]*/i
],
exclude: [
/^https?:\/\/([a-z]+)\.reddit\.com\/ads\/[-\w\.\_\?=]*/i,
/^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*\/submit\/?$/i
],
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
beforeLoad: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
if (!this.options.displayImageCaptions.value) {
RESUtils.addCSS('.imgTitle, .imgCaptions { display: none; }');
}
}
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
this.imageList = [];
this.imagesRevealed = {};
this.dupeAnchors = 0;
/*
true: show all images
false: hide all images
'any string': display images match the tab
*/
this.currentImageTab = false;
this.customImageTabs = {};
if (this.options.markVisited.value) {
// we only need this iFrame hack if we're unable to add to history directly, which Firefox addons and Chrome can do.
if (!BrowserDetect.isChrome() && !BrowserDetect.isFirefox()) {
this.imageTrackFrame = document.createElement('iframe');
this.imageTrackFrame.addEventListener('load', function() {
setTimeout(modules['showImages'].imageTrackShift, 300);
}, false);
this.imageTrackFrame.style.display = 'none';
this.imageTrackFrame.style.width = '0px';
this.imageTrackFrame.style.height = '0px';
document.body.appendChild(this.imageTrackFrame);
}
this.imageTrackStack = [];
}
//set up all site modules
for (var key in this.siteModules) {
this.siteModules[key].go();
}
this.scanningForImages = false;
RESUtils.watchForElement('siteTable', modules['showImages'].findAllImages);
RESUtils.watchForElement('selfText', modules['showImages'].findAllImagesInSelfText);
RESUtils.watchForElement('newComments', modules['showImages'].findAllImagesInSelfText);
this.createImageButtons();
this.findAllImages();
document.addEventListener('dragstart', function(){return false;}, false);
}
},
findAllImagesInSelfText: function(ele) {
modules['showImages'].findAllImages(ele, true);
},
createImageButtons: function() {
if ((location.href.match(/search\?\/?q\=/)) || (location.href.match(/about\/reports/)) || (location.href.match(/about\/spam/)) || (location.href.match(/about\/unmoderated/)) || (location.href.match(/modqueue/)) || (location.href.toLowerCase().match('dashboard'))) {
var hbl = document.body.querySelector('#header-bottom-left');
if (hbl) {
var mainMenuUL = document.createElement('ul');
mainMenuUL.setAttribute('class','tabmenu viewimages');
mainMenuUL.setAttribute('style','display: inline-block');
hbl.appendChild(mainMenuUL);
}
} else {
var mainMenuUL = document.body.querySelector('#header-bottom-left ul.tabmenu');
}
if (mainMenuUL) {
var li = document.createElement('li');
var a = document.createElement('a');
var text = document.createTextNode('scanning for images...');
this.scanningForImages = true;
a.href = 'javascript:void(0);';
a.id = 'viewImagesButton';
a.addEventListener('click', function(e) {
e.preventDefault();
if (!modules['showImages'].scanningForImages) {
modules['showImages'].setShowImages(null, 'image');
}
}, true);
a.appendChild(text);
li.appendChild(a);
mainMenuUL.appendChild(li);
this.viewImageButton = a;
/*
To enable custom image tabs for a subreddit start by adding `[](#/RES_SR_Config/ImageTabs?)` to the markdown code of the sidebar.
This should not have any visible effect on the HTML.
Right now no options have been configured, so there won't be any new tabs.
You can add up to 8 tabs in the following manner:
A tab is defined by a label and a tag list separated by an equals sign like this: `LABEL=TAGLIST`
The label can be up to 32 characters long and may contain english letters, numbers, hyphens, spaces, and underscores. The labels must be URI encoded.
The tag list can contain up to tag values separated by commas. Individual tags have the same content restrictions a labels. (do not URI encode the commmas)
The the tab definitions are joined by ampersands (`&`).
Labels appear to the right of the "view images" button and are surrounded by `[]` brackets.
Post titles are searched for any place that an entry in the tag list appears surrounded by any kind of bracket <>, [], (), {}.
Tags are not case sensitive and whitespace is permitted between the brackets and the tag.
To allow the tabs to be styled, the tabs will have a class that is the tab label with the spaces and hyphens replaced by underscores and then prefixed with `'RESTab-'` so the label 'Feature Request' becomes `'RESTab-feature_request'`.
We realize that the format is highly restrictive, but you must understand that that is for everyone's protection. If there is demand, the filter can be expanded.
Examples:
A hypothetical setup for /r/minecraft that creates tabs for builds, mods, and texture packs:
[](#/RES_SR_Config/ImageTabs?build=build,project&mod=mod&texture%20pack=texture,textures,pack,texture%20pack)
To duplicate the behavior originally used for /r/gonewild you would use:
[](#/RES_SR_Config/ImageTabs?m=m,man,male&f=f,fem,female)
*/
var tabConfig = document.querySelector('.side .md a[href^="#/RES_SR_Config/ImageTabs"]');
//This is hardcoded until the mods of /r/gonewild add the tag
if (!tabConfig && RESUtils.currentSubreddit('gonewild')) {
tabConfig = $('')[0];
}
if (tabConfig) {
var switches = {};
var switchCount = 0;
var whitelist = /^[A-Za-z0-9_ \-]{1,32}$/;
var configString = tabConfig.hash.match(/\?(.*)/);
if (configString != null) {
var pairs = configString[1].split('&');
for (var i = 0; i < pairs.length && switchCount < 8; i++) {
var pair = pairs[i].split('=');
if (pair.length !== 2) continue;
var label = decodeURIComponent(pair[0]);
if (!whitelist.test(label)) continue;
var parts = pair[1].split(',');
var acceptedParts = [];
for (var j = 0; j < parts.length && acceptedParts.length < 8; j++) {
var part = decodeURIComponent(parts[j]);
if (!whitelist.test(part)) continue;
else acceptedParts.push(part);
}
if (acceptedParts.length > 0) {
if (!(label in switches)) switchCount++;
switches[label] = acceptedParts;
}
}
}
if (switchCount > 0) {
for (var key in switches) {
this.customImageTabs[key] = new RegExp('[\\[\\{\\<\\(]\s*('+switches[key].join('|')+')\s*[\\]\\}\\>\\)]','i');
}
}
}
if (!/comments\/[-\w\.\/]/i.test(location.href)) {
for (var mode in this.customImageTabs) {
var li = document.createElement('li');
var a = document.createElement('a');
var text = document.createTextNode('['+mode+']');
a.href = 'javascript:void(0);';
a.className = 'RESTab-'+mode.toLowerCase().replace(/- /g, '_');
a.addEventListener('click', (function(mode) {
return function(e) {
e.preventDefault();
modules['showImages'].setShowImages(mode);
};
})(mode), true);
a.appendChild(text);
li.appendChild(a);
mainMenuUL.appendChild(li);
}
}
}
},
setShowImages: function(newImageTab, type) {
type = type || 'image';
if (newImageTab == null) {
//This is for the all images button
//If we stored `true` then toggle to false, in all other cases turn it to true
if (this.currentImageTab == true) {
this.currentImageTab = false;
} else {
this.currentImageTab = true;
}
} else if (this.currentImageTab == newImageTab) {
//If they are the same, turn it off
this.currentImageTab = false;
} else if (newImageTab in this.customImageTabs) {
//If the tab is defined, switch to it
this.currentImageTab = newImageTab;
} else {
//Otherwise ignore it
return;
}
this.updateImageButtons();
this.updateRevealedImages(type);
},
updateImageButtons: function() {
var imgCount = this.imageList.length;
var showHideText = 'view';
if (this.currentImageTab == true) {
showHideText = 'hide';
}
if (typeof this.viewImageButton !== 'undefined') {
var buttonText = showHideText + ' images ';
if (! RESUtils.currentSubreddit('dashboard')) buttonText += '(' + imgCount + ')';
$(this.viewImageButton).text(buttonText);
}
},
updateRevealedImages: function(type) {
for (var i = 0, len = this.imageList.length; i < len; i++) {
var image = this.imageList[i];
if ($(image).hasClass(type)) {
this.revealImage(image, this.findImageFilter(image.imageLink));
}
}
},
findImageFilter: function(image) {
var isMatched = false;
if (typeof this.currentImageTab === 'boolean') {
//booleans indicate show all or nothing
isMatched = this.currentImageTab;
} else if (this.currentImageTab in this.customImageTabs) {
var re = this.customImageTabs[this.currentImageTab];
isMatched = re.test(image.text);
}
//If false then there is no need to go through the NSFW filter
if (!isMatched) return false;
image.NSFW = false;
if (this.options.hideNSFW.value) {
image.NSFW = /nsfw/i.test(image.text);
}
return !image.NSFW;
},
findAllImages: function(elem, isSelfText) {
modules['showImages'].scanningForImages = true;
if (elem == null) {
elem = document.body;
}
// get elements common across all pages first...
// if we're on a comments page, get those elements too...
var commentsre = /comments\/[-\w\.\/]/i;
var userre = /user\/[-\w\.\/]/i;
modules['showImages'].scanningSelfText = false;
var allElements = [];
if (commentsre.test(location.href) || userre.test(location.href)) {
allElements = elem.querySelectorAll('#siteTable a.title, .expando .usertext-body > div.md a, .content .usertext-body > div.md a');
} else if (isSelfText) {
// We're scanning newly opened (from an expando) selftext...
allElements = elem.querySelectorAll('.usertext-body > div.md a');
modules['showImages'].scanningSelfText = true;
} else {
allElements = elem.querySelectorAll('#siteTable A.title');
}
if (RESUtils.pageType() === 'comments') {
RESUtils.forEachChunked(allElements, 15, 1000, function(element, i, array) {
modules['showImages'].checkElementForImage(element);
if (i >= array.length - 1) {
modules['showImages'].scanningSelfText = false;
modules['showImages'].scanningForImages = false;
modules['showImages'].updateImageButtons(modules['showImages'].imageList.length);
}
});
} else {
var chunkLength = allElements.length;
for (var i = 0; i < chunkLength; i++) {
modules['showImages'].checkElementForImage(allElements[i]);
}
modules['showImages'].scanningSelfText = false;
modules['showImages'].scanningForImages = false;
modules['showImages'].updateImageButtons(modules['showImages'].imageList.length);
}
},
checkElementForImage: function(elem) {
if (this.options.hideNSFW.value) {
if (elem.classList.contains('title')) {
elem.NSFW = elem.parentNode.parentNode.parentNode.classList.contains('over18');
}
} else {
elem.NSFW = false;
}
var href = elem.href;
if ((!elem.classList.contains('imgScanned') && (typeof this.imagesRevealed[href] === 'undefined' || !this.options.ignoreDuplicates.value || (RESUtils.currentSubreddit('dashboard'))) && href !== null) || this.scanningSelfText) {
elem.classList.add('imgScanned');
this.dupeAnchors++;
var siteFound = false;
if (siteFound = this.siteModules['default'].detect(elem)) {
elem.site = 'default';
}
if (!siteFound) {
for (var site in this.siteModules) {
if (site === 'default') continue;
if (this.siteModules[site].detect(elem)) {
elem.site = site;
siteFound = true;
break;
}
}
}
if (siteFound && !elem.NSFW) {
this.imagesRevealed[href] = this.dupeAnchors;
var siteMod = this.siteModules[elem.site];
$.Deferred().resolve(elem).then(siteMod.handleLink).then(siteMod.handleInfo).
then(this.createImageExpando, function(){
console.error.apply(console, arguments);
});
}
} else if (!elem.classList.contains('imgScanned')) {
var textFrag = document.createElement('span');
textFrag.setAttribute('class','RESdupeimg');
$(textFrag).html(' [RES ignored duplicate image]');
insertAfter(elem, textFrag);
}
},
createImageExpando: function(elem) {
var mod = modules['showImages'];
if (!elem) return false;
var href = elem.href;
if (!href) return false;
//This should not be reached in the case of duplicates
elem.name = 'img'+mod.imagesRevealed[href];
//expandLink aka the expando button
var expandLink = document.createElement('a');
expandLink.className = 'toggleImage expando-button collapsedExpando';
if (elem.type === 'IMAGE') expandLink.className += ' image';
if (elem.type === 'GALLERY') expandLink.className += ' image gallery';
if (elem.type === 'TEXT') expandLink.className += ' selftext collapsed';
if (elem.type === 'VIDEO') expandLink.className += ' video collapsed';
if (elem.type === 'AUDIO') expandLink.className += ' video collapsed'; // yes, still class "video", that's what reddit uses.
if (elem.type === 'NOEMBED') expandLink.className += ' '+elem.expandoClass;
if (elem.type === 'GALLERY' && elem.src && elem.src.length) expandLink.setAttribute('title', elem.src.length + ' items in gallery');
$(expandLink).html(' ');
expandLink.addEventListener('click', function(e) {
e.preventDefault();
modules['showImages'].revealImage(e.target, (e.target.classList.contains('collapsedExpando')));
}, true);
var preNode = null;
if (elem.parentNode.classList.contains('title')) {
preNode = elem.parentNode;
expandLink.classList.add('linkImg');
} else {
preNode = elem;
expandLink.classList.add('commentImg');
}
insertAfter(preNode, expandLink);
/*
* save the link element for later use since some extensions
* like web of trust can place other elements in places that
* confuse the old method
*/
expandLink.imageLink = elem;
mod.imageList.push(expandLink);
if (mod.scanningSelfText && mod.options.autoExpandSelfText.value) {
mod.revealImage(expandLink, true);
} else {
// this may have come from an asynchronous call, in which case it'd get missed by findAllImages, so
// if all images are supposed to be visible, expand this link now.
mod.revealImage(expandLink, mod.findImageFilter(expandLink.imageLink));
}
if (mod.scanningForImages == false) {
// also since this may have come from an asynchronous call, we need to update the view images count.
mod.updateImageButtons(mod.imageList.length);
}
},
revealImage: function(expandoButton, showHide) {
if ((!expandoButton) || (! $(expandoButton).is(':visible'))) return false;
// showhide = false means hide, true means show!
var imageLink = expandoButton.imageLink;
if (typeof this.siteModules[imageLink.site] === 'undefined') {
console.log('something went wrong scanning image from site: ' + imageLink.site);
return;
}
if (expandoButton.expandoBox && expandoButton.expandoBox.classList.contains('madeVisible')) {
if (!showHide) {
$(expandoButton).removeClass('expanded').addClass('collapsed collapsedExpando');
expandoButton.expandoBox.style.display = 'none';
if (imageLink.type === 'AUDIO' || imageLink.type === 'VIDEO') {
var mediaTag = expandoButton.expandoBox.querySelector(imageLink.type);
mediaTag.pause();
}
} else {
$(expandoButton).addClass('expanded').removeClass('collapsed collapsedExpando');
expandoButton.expandoBox.style.display = 'block';
var associatedImage = $(expandoButton).data('associatedImage');
if (associatedImage) {
modules['showImages'].syncPlaceholder(associatedImage);
}
}
this.handleSRStyleToggleVisibility();
} else if (showHide) {
//TODO: flash, custom
switch (imageLink.type) {
case 'IMAGE':
case 'GALLERY':
this.generateImageExpando(expandoButton);
break;
case 'TEXT':
this.generateTextExpando(expandoButton);
break;
case 'VIDEO':
this.generateVideoExpando(expandoButton, imageLink.mediaOptions);
break;
case 'AUDIO':
this.generateAudioExpando(expandoButton);
break;
case 'NOEMBED':
this.generateNoEmbedExpando(expandoButton);
break;
}
}
},
generateImageExpando: function(expandoButton) {
var imageLink = expandoButton.imageLink;
var which = imageLink.galleryStart || 0;
var imgDiv = document.createElement('div');
imgDiv.classList.add('madeVisible');
imgDiv.currentImage = which;
imgDiv.sources = [];
// Test for a single image or an album/array of image
if (Array.isArray(imageLink.src)) {
imgDiv.sources = imageLink.src;
// Also preload images for an album
this.preloadImages(imageLink.src, 0);
} else {
// Only the image is left to display, pack it like a single-image album with no caption or title
singleImage = {src:imageLink.src,href:imageLink.href};
imgDiv.sources[0] = singleImage;
}
if ('imageTitle' in imageLink) {
var header = document.createElement('h3');
header.classList.add('imgTitle');
$(header).safeHtml(imageLink.imageTitle);
imgDiv.appendChild(header);
}
if ('imgCaptions' in imageLink) {
var captions = document.createElement('div');
captions.className = 'imgCaptions';
$(captions).safeHtml(imageLink.caption);
imgDiv.appendChild(captions);
}
if ('credits' in imageLink) {
var credits = document.createElement('div');
credits.className = 'imgCredits';
$(credits).safeHtml(imageLink.credits);
imgDiv.appendChild(credits);
}
switch(imageLink.type){
case 'GALLERY':
if (this.options.loadAllInAlbum.value) {
if (imgDiv.sources.length > 1) {
var albumLength = " (" + imgDiv.sources.length + " images)";
$(header).append(albumLength);
}
for (var imgNum = 0; imgNum < imgDiv.sources.length; imgNum++) {
addImage(imgDiv, imgNum, this);
}
break;
} else {
// If we're using the traditional album view, add the controls then fall through to add the IMAGE
var controlWrapper = document.createElement('div');
controlWrapper.className = 'RESGalleryControls';
var leftButton = document.createElement("a");
leftButton.className = 'previous noKeyNav';
leftButton.addEventListener('click', function(e){
var topWrapper = e.target.parentElement.parentElement;
if (topWrapper.currentImage === 0) {
topWrapper.currentImage = topWrapper.sources.length-1;
} else {
topWrapper.currentImage -= 1;
}
adjustGalleryDisplay(topWrapper);
});
controlWrapper.appendChild(leftButton);
var posLabel = document.createElement('span');
posLabel.className = 'RESGalleryLabel';
var niceWhich = ((which+1 < 10)&&(imgDiv.sources.length >= 10)) ? '0'+(which+1) : (which+1);
if (imgDiv.sources.length) {
posLabel.textContent = niceWhich + " of " + imgDiv.sources.length;
} else {
posLabel.textContent = "Whoops, this gallery seems to be empty.";
}
controlWrapper.appendChild(posLabel);
var rightButton = document.createElement("a");
rightButton.className = 'next noKeyNav';
rightButton.addEventListener('click', function(e){
var topWrapper = e.target.parentElement.parentElement;
if (topWrapper.currentImage == topWrapper.sources.length-1) {
topWrapper.currentImage = 0;
} else {
topWrapper.currentImage += 1;
}
adjustGalleryDisplay(topWrapper);
});
controlWrapper.appendChild(rightButton);
if (!imgDiv.sources.length) {
$(leftButton).css('visibility','hidden');
$(rightButton).css('visibility','hidden');
}
imgDiv.appendChild(controlWrapper);
}
case 'IMAGE':
addImage(imgDiv, which, this);
}
function addImage(container, sourceNumber, thisHandle) {
var sourceImage = container.sources[sourceNumber];
var paragraph = document.createElement('p');
if (!sourceImage) {
return;
}
if ('title' in sourceImage) {
var imageTitle = document.createElement('h4');
imageTitle.className = 'imgCaptions';
$(imageTitle).safeHtml(sourceImage.title);
paragraph.appendChild(imageTitle);
}
if ('caption' in sourceImage) {
var imageCaptions = document.createElement('div');
imageCaptions.className = 'imgCaptions';
$(imageCaptions).safeHtml(sourceImage.caption);
paragraph.appendChild(imageCaptions);
}
var imageAnchor = document.createElement('a');
imageAnchor.classList.add('madeVisible');
imageAnchor.href = sourceImage.href;
if (thisHandle.options.openInNewWindow.value) {
imageAnchor.target ='_blank';
}
var image = document.createElement('img');
$(expandoButton).data('associatedImage', image);
//Unfortunately it is impossible to use a global event handler for these.
image.onerror = function() {
image.classList.add("RESImageError");
};
image.onload = function() {
image.classList.remove("RESImageError");
};
image.classList.add('RESImage');
image.id = 'RESImage-' + RESUtils.randomHash();
image.src = sourceImage.src;
image.title = 'drag to resize';
image.style.maxWidth = thisHandle.options.maxWidth.value + 'px';
image.style.maxHeight = thisHandle.options.maxHeight.value + 'px';
imageAnchor.appendChild(image);
modules['showImages'].setPlaceholder(image);
thisHandle.makeImageZoomable(image);
thisHandle.trackImageLoad(imageLink, image);
paragraph.appendChild(imageAnchor);
container.appendChild(paragraph);
}
//Adjusts the images for the gallery navigation buttons as well as the "n of m" display.
function adjustGalleryDisplay(topLevel) {
var source = topLevel.sources[topLevel.currentImage];
var image = topLevel.querySelector('img.RESImage');
var imageAnchor = image.parentElement;
var paragraph = imageAnchor.parentElement;
image.src = source.src;
imageAnchor.href = source.href || imageLink.href;
var paddedImageNumber = ((topLevel.currentImage+1 < 10)&&(imgDiv.sources.length >= 10)) ? '0'+(topLevel.currentImage+1) : topLevel.currentImage+1;
if (imgDiv.sources.length) {
topLevel.querySelector('.RESGalleryLabel').textContent = (paddedImageNumber+" of "+imgDiv.sources.length);
} else {
topLevel.querySelector('.RESGalleryLabel').textContent = "Whoops, this gallery seems to be empty.";
}
if (topLevel.currentImage === 0) {
leftButton.classList.add('end');
rightButton.classList.remove('end');
} else if (topLevel.currentImage === topLevel.sources.length-1) {
leftButton.classList.remove('end');
rightButton.classList.add('end');
} else {
leftButton.classList.remove('end');
rightButton.classList.remove('end');
}
$(paragraph).find('.imgCaptions').empty();
var imageTitle = paragraph.querySelector('h4.imgCaptions');
if (imageTitle) $(imageTitle).safeHtml(source.title);
var imageCaptions = paragraph.querySelector('div.imgCaptions');
if (imageCaptions) $(imageCaptions).safeHtml(source.caption);
}
if (expandoButton.classList.contains('commentImg')) {
insertAfter(expandoButton, imgDiv);
} else {
expandoButton.parentNode.appendChild(imgDiv);
}
expandoButton.expandoBox = imgDiv;
expandoButton.classList.remove('collapsedExpando');
expandoButton.classList.add('expanded');
},
/**
* Recursively loads the images synchronously.
*/
preloadImages: function(srcs, i) {
var _this = this,
_i = i,
img = new Image();
img.onload = img.onerror = function(){
_i++;
if(typeof srcs[_i] === 'undefined'){
return;
}
_this.preloadImages(srcs, _i);
delete img; // Delete the image element from the DOM to stop the RAM usage getting to high.
}
img.src = srcs[i].src;
},
generateTextExpando: function(expandoButton) {
var imageLink = expandoButton.imageLink;
var wrapperDiv = document.createElement('div');
wrapperDiv.className = 'usertext';
var imgDiv = document.createElement('div');
imgDiv.className = 'madeVisible usertext-body';
var header = document.createElement('h3');
header.className = 'imgTitle';
$(header).safeHtml(imageLink.imageTitle);
imgDiv.appendChild(header);
var text = document.createElement('div');
text.className = 'md';
$(text).safeHtml(imageLink.src);
imgDiv.appendChild(text);
var captions = document.createElement('div');
captions.className = 'imgCaptions';
$(captions).safeHtml(imageLink.caption);
imgDiv.appendChild(captions);
if ('credits' in imageLink) {
var credits = document.createElement('div');
credits.className = 'imgCredits';
$(credits).safeHtml(imageLink.credits);
imgDiv.appendChild(credits);
}
wrapperDiv.appendChild(imgDiv);
if (expandoButton.classList.contains('commentImg')) {
insertAfter(expandoButton, wrapperDiv);
} else {
expandoButton.parentNode.appendChild(wrapperDiv);
}
expandoButton.expandoBox = imgDiv;
expandoButton.classList.remove('collapsedExpando');
expandoButton.classList.remove('collapsed');
expandoButton.classList.add('expanded');
//TODO: Decide how to handle history for this.
//Selfposts already don't mark it, so either don't bother or add marking for selfposts.
},
generateVideoExpando: function(expandoButton, options) {
var imageLink = expandoButton.imageLink;
var wrapperDiv = document.createElement('div');
wrapperDiv.className = 'usertext';
var imgDiv = document.createElement('div');
imgDiv.className = 'madeVisible usertext-body';
var header = document.createElement('h3');
header.className = 'imgTitle';
$(header).safeHtml(imageLink.imageTitle);
imgDiv.appendChild(header);
var video = document.createElement('video');
video.addEventListener('click', modules['showImages'].handleVideoClick);
video.setAttribute('controls','');
video.setAttribute('preload','');
if (options) {
if (options.autoplay) {
video.setAttribute('autoplay','');
}
if (options.muted) {
video.setAttribute('muted','');
}
if (options.loop) {
video.setAttribute('loop','');
}
}
var sourcesHTML = "",
sources = $(imageLink).data('sources'),
source, sourceEle;
for (var i=0, len=sources.length; i -1) {
var hashes = groups[1].split(/[&,]/);
def.resolve(elem, {
album: {images: hashes.map(function(hash) {
return {
image: {title: '', caption: '', hash: hash},
links: {original: 'http://i.imgur.com/'+hash+'.jpg'}
};
})}
});
} else {
// removed caption API calls as they don't seem to exist/matter for single images, only albums...
//If we don't show captions, then we can skip the API call.
def.resolve(elem, {image: {
links: {
//Imgur doesn't really care about the extension and the browsers don't seem to either.
original: 'http://i.imgur.com/'+groups[1]+'.jpg'
}, image: {}}
});
}
} else if (albumGroups && !albumGroups[2]) {
var apiURL = siteMod.apiPrefix + 'album/' + albumGroups[1] + '.json';
elem.imgHash = albumGroups[1];
if (apiURL in siteMod.calls) {
if (siteMod.calls[apiURL] != null) {
def.resolve(elem, siteMod.calls[apiURL]);
} else {
def.reject();
}
} else {
GM_xmlhttpRequest({
method: 'GET',
url: apiURL,
// aggressiveCache: true,
onload: function(response) {
try {
var json = JSON.parse(response.responseText);
siteMod.calls[apiURL] = json;
def.resolve(elem, json);
} catch (error) {
siteMod.calls[apiURL] = null;
def.reject();
}
},
onerror: function(response) {
def.reject();
}
});
}
} else {
def.reject();
}
return def.promise();
},
handleInfo: function(elem, info) {
if ('image' in info) {
return modules['showImages'].siteModules['imgur'].handleSingleImage(elem, info);
} else if ('album' in info) {
return modules['showImages'].siteModules['imgur'].handleGallery(elem, info);
} else if (info.error && info.error.message === 'Album not found') {
// This case comes up when there is an imgur.com/gallery/HASH link that
// links to an image, not an album (not to be confused with the word "gallery", ugh)
info = {
image: {
links: {
original: 'http://i.imgur.com/' + elem.imgHash + '.jpg'
},
image: {}
}
};
return modules['showImages'].siteModules['imgur'].handleSingleImage(elem, info);
} else {
return $.Deferred().reject().promise();
// console.log("ERROR", info);
// console.log(arguments.callee.caller);
}
},
handleSingleImage: function(elem, info) {
elem.src = info.image.links.original;
elem.href = info.image.links.original;
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
elem.type = 'IMAGE';
if (info.image.image.caption) elem.caption = info.image.image.caption;
return $.Deferred().resolve(elem).promise();
},
handleGallery: function(elem, info) {
var base = elem.href.split('#')[0];
elem.src = info.album.images.map(function(e, i, a) {
return {
title: e.image.title,
src: e.links.original,
href: base + '#' + e.image.hash,
caption: e.image.caption
};
});
if (elem.hash) {
var hash = elem.hash.slice(1);
if (isNaN(hash)) {
for (var i = 0; i < elem.src.length; i++) {
if (hash == info.album.images[i].image.hash) {
elem.galleryStart = i;
break;
}
}
} else {
elem.galleryStart = parseInt(hash, 10);
}
}
elem.imageTitle = info.album.title;
elem.caption = info.album.description;
elem.type = 'GALLERY';
return $.Deferred().resolve(elem).promise();
}
},
gfycat: {
calls: {},
go: function() {
},
detect: function(elem) {
var href = elem.href.toLowerCase();
return href.indexOf('gfycat.com') !== -1 && href.substring(-1) !== '+';
},
handleLink: function(elem) {
var hashRe = /^http:\/\/[a-zA-Z0-9\-\.]*gfycat\.com\/(\w+)\.?/i;
var def = $.Deferred();
var groups = hashRe.exec(elem.href);
if (!groups) return def.reject();
var href = elem.href.toLowerCase();
var hotLink = false;
if(href.indexOf('giant.gfycat') !== -1 ||
href.indexOf('fat.gfycat') !== -1 ||
href.indexOf('zippy.gfycat') !== -1)
hotLink = true;
var siteMod = modules['showImages'].siteModules['gfycat'];
var apiURL = 'http://gfycat.com/cajax/get/' + groups[1];
if (apiURL in siteMod.calls) {
if (siteMod.calls[apiURL] != null) {
def.resolve(elem, siteMod.calls[apiURL]);
} else {
siteMod.calls[apiURL] = null;
def.reject();
}
} else {
GM_xmlhttpRequest({
method: 'GET',
url: apiURL,
aggressiveCache: true,
onload: function(response) {
try {
var json = JSON.parse(response.responseText);
json.gfyItem.src = elem.href;
json.gfyItem.hotLink = hotLink
siteMod.calls[apiURL] = json;
def.resolve(elem, json.gfyItem);
} catch(error) {
siteMod.calls[apiURL] = null;
def.reject()
}
},
onerror: function(response) {
def.reject();
}
});
}
return def.promise();
},
handleInfo: function(elem, info) {
function humanSize(bytes) {
var byteUnits = [' kB', ' MB'];
for(var i=-1; bytes > 1024; i++) {
bytes = bytes / 1024;
}
return Math.max(bytes, 0.1).toFixed(1) + byteUnits[i];
}
if(info.hotLink) {
elem.type="IMAGE";
elem.src = info.src;
elem.imageTitle = humanSize(info.gifSize);
if(((info.gifSize > 524288 && (info.gifSize / info.mp4Size) > 5)
|| (info.gifSize > 1048576 && (info.gifSize / info.mp4Size) > 2))
&& document.createElement('video').canPlayType)
elem.imageTitle += ' (' + humanSize(info.mp4Size) + " for HTML5 version.)";
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
return $.Deferred().resolve(elem).promise();
}
elem.mediaOptions = {
autoplay: true,
loop: true
}
sources = [];
sources[0] = {'file': info.mp4Url,'type':'video/mp4'};
sources[1] = {'file': info.webmUrl,'type':'video/webm'};
elem.type = 'VIDEO';
$(elem).data('sources', sources);
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
return $.Deferred().resolve(elem).promise();
}
},
ehost: {
go: function() {},
detect: function(elem) {
var href = elem.href.toLowerCase();
return href.indexOf('eho.st') !== -1 && href.substring(-1) !== '+';
},
handleLink: function(elem) {
var hashRe = /^http:\/\/(?:i\.)?(?:\d+\.)?eho\.st\/(\w+)\/?/i;
var def = $.Deferred();
var groups = hashRe.exec(elem.href);
if (groups) {
def.resolve(elem, {
src: 'http://i.eho.st/'+groups[1]+'.jpg'
});
} else {
def.reject();
}
return def.promise();
},
handleInfo: function(elem, info) {
elem.type = 'IMAGE';
elem.src = info.src;
elem.href = info.src;
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
elem.onerror = function() {
if (this.src.match(/\.jpg/)) {
this.src = this.src.slice(0, elem.src.length - 3) + 'png';
} else if (this.src.match(/\.png/)) {
this.src = this.src.slice(0, elem.src.length - 3) + 'gif';
}
}
return $.Deferred().resolve(elem).promise();
}
},
picsarus: {
go: function() {},
detect: function(elem) {
var href = elem.href.toLowerCase();
return href.indexOf('picsarus.com') !== -1 && href.substring(-1) !== '+';
},
handleLink: function(elem) {
var hashRe = /^https?:\/\/(?:i\.|edge\.|www\.)*picsarus\.com\/(?:r\/[\w]+\/)?([\w]{6,})(\..+)?$/i;
var def = $.Deferred();
var groups = hashRe.exec(elem.href);
if (groups) {
def.resolve(elem, {
src: 'http://www.picsarus.com/'+groups[1]+'.jpg'
});
} else {
def.reject();
}
return def.promise();
},
handleInfo: function(elem, info) {
elem.type = 'IMAGE';
elem.src = info.src;
elem.href = info.src;
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
return $.Deferred().resolve(elem).promise();
}
},
snaggy: {
go: function() {},
detect: function(elem) {
return elem.href.toLowerCase().indexOf('snag.gy/') !== -1;
},
handleLink: function(elem) {
var def = $.Deferred();
var href = elem.href;
var extensions = ['.jpg','.png','.gif'];
if (href.indexOf('i.snag') === -1) href = href.replace('snag.gy', 'i.snag.gy');
if (extensions.indexOf(href.substr(-4)) === -1) href = href+'.jpg';
def.resolve(elem, {src: href});
return def.promise();
},
handleInfo: function(elem, info) {
elem.type = 'IMAGE';
elem.src = info.src;
elem.href = info.src;
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
return $.Deferred().resolve(elem).promise();
}
},
picshd: {
go: function() {},
detect: function(elem) {
var href = elem.href.toLowerCase();
return href.indexOf('picshd.com/') !== -1;
},
handleLink: function(elem) {
var def = $.Deferred();
var hashRe = /^https?:\/\/(?:i\.|edge\.|www\.)*picshd\.com\/([\w]{5,})(\..+)?$/i;
var groups = hashRe.exec(elem.href);
if (groups) {
def.resolve(elem, 'http://i.picshd.com/'+groups[1]+'.jpg');
} else {
def.reject();
}
return def.promise();
},
handleInfo: function(elem, info) {
elem.type = 'IMAGE';
elem.src = info;
elem.href = info;
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
return $.Deferred().resolve(elem).promise();
}
},
minus: {
calls: {},
go: function() {},
detect: function(elem) {
var href = elem.href.toLowerCase();
return href.indexOf('min.us') !== -1 && href.indexOf('blog.') === -1;
},
handleLink: function(elem) {
var def = $.Deferred();
var hashRe = /^http:\/\/min\.us\/([\w]+)(?:#[\d+])?$/i;
var href = elem.href.split('?')[0];
//TODO: just make default run first and remove this
var getExt = href.split('.');
var ext = (getExt.length > 1?getExt[getExt.length - 1].toLowerCase():'');
if (['jpg', 'jpeg', 'png', 'gif'].indexOf(ext) !== -1) {
var groups = hashRe.exec(href);
if (groups && !groups[2]) {
var hash = groups[1];
if (hash.substr(0, 1) === 'm') {
var apiURL = 'http://min.us/api/GetItems/' + hash;
var calls = modules['showImages'].siteModules['minus'].calls;
if (apiURL in calls) {
if (calls[apiURL] != null) {
def.resolve(elem, calls[apiURL]);
} else {
def.reject();
}
} else {
GM_xmlhttpRequest({
method: 'GET',
url: apiURL,
onload: function(response) {
try {
var json = JSON.parse(response.responseText);
modules['showImages'].siteModules['minus'].calls[apiURL] = json;
def.resolve(elem, json);
} catch (e) {
modules['showImages'].siteModules['minus'].calls[apiURL] = null;
def.reject();
}
},
onerror: function(response) {
def.reject();
}
});
}
} else { // if not 'm', not a gallery, we can't do anything with the API.
def.reject();
}
} else {
def.reject();
}
} else {
def.reject();
}
return def.promise();
},
handleInfo: function(elem, info) {
var def = $.Deferred();
//TODO: Handle titles
//TODO: Handle possibility of flash items
if ('ITEMS_GALLERY' in info) {
if (info.ITEMS_GALLERY.length > 1) {
elem.type = 'GALLERY';
elem.src = {
src: info.ITEMS_GALLERY
};
} else {
elem.type = 'IMAGE';
elem.href = info.ITEMS_GALLERY[0];
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
elem.src = info.ITEMS_GALLERY[0];
}
def.resolve(elem);
} else {
def.reject();
}
return def.promise()
}
},
flickr: {
go: function() {},
detect: function(elem) {
var hashRe = /^http:\/\/(?:\w+)\.?flickr\.com\/(?:.*)\/([\d]{10})\/?(?:.*)?$/i;
var href = elem.href;
return hashRe.test(href);
},
handleLink: function(elem) {
var def = $.Deferred();
// modules['showImages'].createImageExpando(elem);
// var selector = '#allsizes-photo > IMG';
var href = elem.href;
if (href.indexOf('/sizes') === -1) {
var inPosition = href.indexOf('/in/');
var inFragment = '';
if (inPosition !== -1) {
inFragment = href.substring(inPosition);
href = href.substring(0, inPosition);
}
href += '/sizes/c' + inFragment;
}
href = href.replace('/lightbox', '');
href = 'http://www.flickr.com/services/oembed/?format=json&url=' + href;
GM_xmlhttpRequest({
method: 'GET',
url: href,
onload: function(response) {
try {
var json = JSON.parse(response.responseText);
def.resolve(elem, json);
} catch (e) {
def.reject();
}
},
onerror: function(response) {
def.reject();
}
})
return def.promise();
},
handleInfo: function(elem, info) {
var def = $.Deferred();
var imgRe = /\.(jpg|jpeg|gif|png)/i;
if ('url' in info) {
elem.imageTitle = info.title;
var original_url = elem.href;
if(imgRe.test(info.url)) {
elem.src = info.url;
// elem.href = info.url;
} else {
elem.src = info.thumbnail_url;
// elem.href = info.thumbnail_url;
}
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
elem.credits = 'Picture by: '+info.author_name+' @ Flickr';
elem.type = 'IMAGE';
def.resolve(elem);
} else {
def.reject()
}
return def.promise();
}
},
steam: {
go: function() {},
detect: function(elem) {
return elem.href.toLowerCase().indexOf('cloud.steampowered.com') !== -1;
},
handleLink: function(elem) {
return $.Deferred().resolve(elem, elem.href).promise();
},
handleInfo: function(elem, info) {
elem.type = 'IMAGE';
elem.src = info;
elem.href = info;
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
}
return $.Deferred().resolve(elem).promise();
}
},
deviantart: {
calls: {},
matchRe: /^http:\/\/(?:fav\.me\/.*|(?:.+\.)?deviantart\.com\/(?:art\/.*|[^#]*#\/d.*))$/i,
go: function() {},
detect: function(elem) {
return modules['showImages'].siteModules['deviantart'].matchRe.test(elem.href);
},
handleLink: function(elem) {
var def = $.Deferred();
var siteMod = modules['showImages'].siteModules['deviantart'];
var apiURL = 'http://backend.deviantart.com/oembed?url=' + encodeURIComponent(elem.href);
if (apiURL in siteMod.calls) {
if (siteMod.calls[apiURL] != null) {
def.resolve(elem, siteMod.calls[apiURL]);
} else {
def.reject();
}
} else {
GM_xmlhttpRequest({
method: 'GET',
url: apiURL,
// aggressiveCache: true,
onload: function(response) {
try {
var json = JSON.parse(response.responseText);
siteMod.calls[apiURL] = json;
def.resolve(elem, json);
} catch(error) {
siteMod.calls[apiURL] = null;
def.reject()
}
},
onerror: function(response) {
def.reject();
}
});
}
return def.promise();
},
handleInfo: function(elem, info) {
var def = $.Deferred();
if ('url' in info) {
elem.imageTitle = info.title;
var original_url = elem.href;
if(['jpg', 'jpeg', 'png', 'gif'].indexOf(info.url) !== -1) {
elem.src = info.url;
// elem.href = info.url;
} else {
elem.src = info.thumbnail_url;
// elem.href = info.thumbnail_url;
}
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
// elem.credits = 'Original link: '+original_url+' Art by: '+info.author_name+' @ deviantART';
elem.credits = 'Art by: '+info.author_name+' @ deviantART';
elem.type = 'IMAGE';
def.resolve(elem);
} else {
def.reject()
}
return def.promise();
}
},
tumblr: {
calls: {},
APIKey: 'WeJQquHCAasi5EzaN9jMtIZkYzGfESUtEvcYDeSMLICveo3XDq',
matchRE: /^https?:\/\/([a-z0-9\-]+\.tumblr\.com)\/post\/(\d+)(?:\/.*)?$/i,
go: function() { },
detect: function(elem) {
return modules['showImages'].siteModules['tumblr'].matchRE.test(elem.href);
},
handleLink: function(elem) {
var def = $.Deferred();
var siteMod = modules['showImages'].siteModules['tumblr'];
var groups = siteMod.matchRE.exec(elem.href);
if (groups) {
var apiURL = 'http://api.tumblr.com/v2/blog/'+groups[1]+'/posts?api_key='+siteMod.APIKey+'&id='+groups[2] + '&filter=raw';
if (apiURL in siteMod.calls) {
if (siteMod.calls[apiURL] != null) {
def.resolve(elem, siteMod.calls[apiURL]);
} else {
def.reject();
}
} else {
GM_xmlhttpRequest({
method:'GET',
url: apiURL,
// aggressiveCache: true,
onload: function(response) {
try {
var json = JSON.parse(response.responseText);
if ('meta' in json && json.meta.status === 200) {
siteMod.calls[apiURL] = json;
def.resolve(elem, json);
} else {
siteMod.calls[apiURL] = null;
def.reject();
}
} catch (error) {
siteMod.calls[apiURL] = null;
def.reject();
}
},
onerror: function(response) {
def.reject();
}
});
}
} else {
def.reject();
}
return def.promise();
},
handleInfo: function(elem, info) {
var def = $.Deferred();
var original_url = elem.href;
var post = info.response.posts[0];
switch (post.type) {
case 'photo':
if (post.photos.length > 1) {
elem.type = 'GALLERY';
elem.src = post.photos.map(function(e) {
return {
src: e.original_size.url,
caption: e.caption
};
});
} else {
elem.type = "IMAGE";
elem.src = post.photos[0].original_size.url;
}
break;
case 'text':
elem.type = 'TEXT';
elem.imageTitle = post.title;
if (post.format === 'markdown') {
elem.src = modules['commentPreview'].converter.render(post.body)
} else if (post.format === 'html') {
elem.src = post.body;
}
break;
default:
return def.reject().promise();
break;
}
elem.caption = post.caption;
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
elem.credits = 'Posted by: '+info.response.blog.name+' @ Tumblr';
def.resolve(elem);
return def.promise()
}
},
memecrunch: {
go: function() {},
detect: function(elem) {
return elem.href.toLowerCase().indexOf('memecrunch.com') !== -1;
},
handleLink: function(elem) {
var def = $.Deferred();
var hashRe = /^http:\/\/memecrunch\.com\/meme\/([0-9A-Z]+)\/([\w\-]+)(\/image\.(png|jpg))?/i;
var groups = hashRe.exec(elem.href);
if (groups && typeof groups[1] !== 'undefined') {
def.resolve(elem, 'http://memecrunch.com/meme/'+groups[1]+'/'+(groups[2]||'null')+'/image.png');
} else {
def.reject();
}
return def.promise();
},
handleInfo: function(elem, info) {
elem.type = 'IMAGE';
elem.src = info;
elem.href = info;
if (RESUtils.pageType() === 'linklist') {
$(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
}
modules['showImages'].createImageExpando(elem);
}
},
mediacrush: {
calls: {},
go: function() {},
detect: function(elem) {
return elem.href.toLowerCase().indexOf('mediacru.sh') !== -1;
},
handleLink: function(elem) {
var hashRe = /^https?:\/\/(?:www\.)?mediacru\.sh\/([\w-]+)$/i;
var def = $.Deferred();
var groups = hashRe.exec(elem.href);
if (!groups) return def.reject();
var siteMod = modules['showImages'].siteModules['mediacrush'];
var apiURL = 'https://mediacru.sh/' + encodeURIComponent(groups[1]) + '.json';
if (apiURL in siteMod.calls) {
if (siteMod.calls[apiURL] != null) {
def.resolve(elem, siteMod.calls[apiURL]);
} else {
siteMod.calls[apiURL] = null;
def.reject();
}
} else {
GM_xmlhttpRequest({
method: 'GET',
url: apiURL,
// aggressiveCache: true,
onload: function(response) {
try {
var json = JSON.parse(response.responseText);
siteMod.calls[apiURL] = json;
def.resolve(elem, json);
} catch(error) {
siteMod.calls[apiURL] = null;
def.reject()
}
},
onerror: function(response) {
def.reject();
}
});
}
return def.promise();
},
handleInfo: function(elem, info) {
var def = $.Deferred()
// check files to see if video or image
elem.type = 'IMAGE';
var mediaData = {
src: 'http://mediacru.sh/'+info.original,
sources: []
}
if (info.original.indexOf('.gif') !== -1) {
elem.mediaOptions = {
autoplay: true,
muted: true,
loop: true
}
}
for (var i=0, len=info.files.length; i" + linkKarma + " " + modules['showKarma'].options.separator.value + " " + commentKarma + "");
}
}
};
modules['hideChildComments'] = {
moduleID: 'hideChildComments',
moduleName: 'Hide All Child Comments',
category: 'Comments',
options: {
// any configurable options you have go here...
// options must have a type and a value..
// valid types are: text, boolean (if boolean, value must be true or false)
// for example:
automatic: {
type: 'boolean',
value: false,
description: 'Automatically hide all but parent comments, or provide a link to hide them all?'
}
},
description: 'Allows you to hide all comments except for replies to the OP for easier reading.',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
include: [
/^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
/^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
],
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
var toggleButton = document.createElement('li');
this.toggleAllLink = document.createElement('a');
this.toggleAllLink.textContent = 'hide all child comments';
this.toggleAllLink.setAttribute('action','hide');
this.toggleAllLink.setAttribute('href','javascript:void(0);');
this.toggleAllLink.setAttribute('title','Show only replies to original poster.');
this.toggleAllLink.addEventListener('click', function() {
modules['hideChildComments'].toggleComments(this.getAttribute('action'));
if (this.getAttribute('action') === 'hide') {
this.setAttribute('action','show');
this.setAttribute('title','Show all comments.');
this.textContent = 'show all child comments';
} else {
this.setAttribute('action','hide');
this.setAttribute('title','Show only replies to original poster.');
this.textContent = 'hide all child comments';
}
}, true);
toggleButton.appendChild(this.toggleAllLink);
var commentMenu = document.querySelector('ul.buttons');
if (commentMenu) {
commentMenu.appendChild(toggleButton);
var rootComments = document.querySelectorAll('div.commentarea > div.sitetable > div.thing > div.child > div.listing');
for (var i=0, len=rootComments.length; i div.sitetable > div.thing');
for (var i=0, len=commentContainers.length; i div.sitetable');
var thisToggleLink = commentContainers[i].querySelector('a.toggleChildren');
if (thisToggleLink !== null) {
if (action === 'hide') {
if (thisChildren !== null) {
thisChildren.style.display = 'none'
}
thisToggleLink.textContent = 'show child comments';
// thisToggleLink.setAttribute('title','show child comments');
thisToggleLink.setAttribute('action','show');
} else {
if (thisChildren !== null) {
thisChildren.style.display = 'block';
}
thisToggleLink.textContent = 'hide child comments';
// thisToggleLink.setAttribute('title','hide child comments');
thisToggleLink.setAttribute('action','hide');
}
}
}
}
}
};
modules['showParent'] = {
moduleID: 'showParent',
moduleName: 'Show Parent on Hover',
category: 'Comments',
options: {
hoverDelay: {
type: 'text',
value: 500,
description: 'Delay, in milliseconds, before parent hover loads. Default is 500.'
},
fadeDelay: {
type: 'text',
value: 200,
description: 'Delay, in milliseconds, before parent hover fades away after the mouse leaves. Default is 200.'
},
fadeSpeed: {
type: 'text',
value: 0.3,
description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
},
direction: {
type: 'enum',
value: 'down',
values: [
{ name: 'Up', value: 'up' },
{ name: 'Down', value: 'down' }
],
description: 'Order the parent comments to place the closest parent at the top (down) or at the bottom (up).'
},
},
description: 'Shows the parent comments when hovering over the "parent" link of a comment.',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
include: [
/^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
/^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
],
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
if (modules['showParent'].options.direction.value === 'up') {
document.html.classList.add('res-parents-up');
} else {
document.html.classList.add('res-parents-down');
}
$('body').on('mouseenter', '.comment .buttons :not(:first-child) .bylink', function(e) {
RESUtils.hover.begin(this, {
openDelay: modules['showParent'].options.hoverDelay.value,
fadeDelay: modules['showParent'].options.fadeDelay.value,
fadeSpeed: modules['showParent'].options.fadeSpeed.value
}, modules['showParent'].showCommentHover , {});
});
$('body').on('click', '#RESHoverContainer .parentCommentWrapper .arrow', modules['showParent'].handleVoteClick);
}
},
handleVoteClick: function(evt) {
var voteKey = {'-1':'disliked', 0:'unvoted', 1:'liked'};
var id = $(this).parent().parent().attr('data-fullname');
var direction = /(up|down)(mod)?/.exec(this.className);
if (direction) direction = direction[1];
else return;
var targetButton = $('.content .thing.id-'+id+' > .midcol').find('.arrow.'+direction + ', .arrow.'+direction+'mod');
if (targetButton.length !== 1) {
console.error("When attempting to find %s arrow for comment %s %d elements were returned", direction, id, targetButton.length);
return;
}
targetButton.click();
var clickedDir = (direction==='up'?1:-1);
var startDir;
var mid = $(this).parent();
if (mid.hasClass('unvoted')) startDir = 0;
else if (mid.hasClass('likes')) startDir = 1;
else if (mid.hasClass('dislikes')) startDir = -1;
var newDir = clickedDir===startDir ? 0 : clickedDir;
mid.parent().children('.'+voteKey[startDir]).removeClass(voteKey[startDir]).addClass(voteKey[newDir])
mid.find('.up, .upmod')
.toggleClass('upmod', clickedDir === 1)
.toggleClass('up', clickedDir !== 1);
mid.find('.down, .downmod')
.toggleClass('downmod', clickedDir === -1)
.toggleClass('down', clickedDir !== -1);
},
showCommentHover: function(def, base, context) {
var direction = modules['showParent'].options.direction.value;
var thing = $(base);
//If the passed element is not a `.thing` move up to the nearest `.thing`
if (!$(thing).is('.thing')) thing = thing.parents('.thing:first');
var parents = $(thing).parents('.thing').clone();
if (direction === 'up') {
parents = $(parents.get().reverse());
}
parents.addClass('comment parentComment').removeClass('thing even odd');
parents.children('.child').remove(); // replies and reply edit form
parents.each(function(i) {
//A link to go to the actual comment
var id = $(this).attr('data-fullname');
if (id != undefined) {
id = id.slice(3)
$(this).find('> .entry .tagline').append('goto comment');
}
});
parents.find('.parent').remove();
parents.find('.usertext-body').show(); // contents
parents.find('.flat-list.buttons').remove(); // buttons
parents.find('.usertext-edit').remove(); // edit form
parents.find('.RESUserTag').remove(); // tags
parents.find('.voteWeight').remove(); // tags
parents.find('.entry').removeClass('RES-keyNav-activeElement');
parents.find('.author.userTagged').removeClass('userTagged'); //tags again
parents.find('.collapsed').remove(); //unused collapse view
parents.find('.expand').remove(); //expand vutton
parents.find('form').attr('id', ''); //ID's should be unique
parents.find('.arrow').attr('onclick', ''); //clear the vote handlers
/*
I am stripping out the image viewer stuff for now.
Making the image viewer work here requires some changes that are for another time.
*/
parents.find('.madeVisible, .toggleImage').remove(); //image viewer
parents.find('.keyNavAnnotation').remove();
var container = $('
');
def.resolve("Parents", container);
}
};
modules['neverEndingReddit'] = {
moduleID: 'neverEndingReddit',
moduleName: 'Never Ending Reddit',
category: 'UI',
options: {
// any configurable options you have go here...
// options must have a type and a value..
returnToPrevPage: {
type: 'boolean',
value: true,
description: 'Return to the page you were last on when hitting "back" button?'
},
autoLoad: {
type: 'boolean',
value: true,
description: 'Automatically load new page on scroll (if off, you click to load)'
},
notifyWhenPaused: {
type: 'boolean',
value: true,
description: 'Show a reminder to unpause Never-Ending Reddit after pausing'
},
reversePauseIcon: {
type: 'boolean',
value: false,
description: 'Show "paused" bars icon when auto-load is paused and "play" wedge icon when active'
},
pauseAfterEvery: {
type: 'text',
value: 0,
description: 'After auto-loading a certain number of pages, pause the auto-loader'
+ '
0 or a negative number means Never-Ending Reddit will only pause when you click'
+ ' the play/pause button in the top right corner.'
},
hideDupes: {
type: 'enum',
value: 'fade',
values: [
{ name: 'Fade', value: 'fade' },
{ name: 'Hide', value: 'hide' },
{ name: 'Do not hide', value: 'none' }
],
description: 'Fade or completely hide duplicate posts from previous pages.'
}
},
description: 'Inspired by modules like River of Reddit and Auto Pager - gives you a never ending stream of reddit goodness.',
isEnabled: function() {
return RESConsole.getModulePrefs(this.moduleID);
},
include: [
/^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\_\?=]*/i
],
exclude: [
],
isMatchURL: function() {
return RESUtils.isMatchURL(this.moduleID);
},
beforeLoad: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
RESUtils.addCSS('#NERModal { display: none; z-index: 999; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: #333; opacity: 0.6; }');
RESUtils.addCSS('#NERContent { display: none; position: fixed; top: 40px; z-index: 1000; width: 720px; background-color: #FFF; color: #000; padding: 10px; font-size: 12px; }');
RESUtils.addCSS('#NERModalClose { position: absolute; top: 3px; right: 3px; }');
RESUtils.addCSS('#NERFail { min-height: 30px; width: 95%; font-size: 14px; border: 1px solid #999; border-radius: 10px; padding: 5px; text-align: center; bgcolor: #f0f3fc; cursor: pointer; }');
RESUtils.addCSS('.NERdupe p.title:after { color: #000; font-size: 10px; content: \' (duplicate from previous page)\'; }');
RESUtils.addCSS('.NERPageMarker { text-align: center; color: #7f7f7f; font-size: 14px; margin-top: 6px; margin-bottom: 6px; font-weight: normal; background-color: #f0f3fc; border: 1px solid #c7c7c7; border-radius: 3px; padding: 3px 0; }');
// hide next/prev page indicators
RESUtils.addCSS('.content p.nextprev { display: none; } ');
switch (this.options.hideDupes.value) {
case 'fade':
RESUtils.addCSS('.NERdupe { opacity: 0.3; }');
break;
case 'hide':
RESUtils.addCSS('.NERdupe { display: none; }');
break;
}
// set the style for our little loader widget
RESUtils.addCSS('#progressIndicator { width: 95%; min-height: 20px; margin: 0 auto; font-size: 14px; border: 1px solid #999; border-radius: 10px; padding: 10px; text-align: center; background-color: #f0f3fc; cursor: pointer; } ');
RESUtils.addCSS('#progressIndicator h2 { margin-bottom: .5em; }');
RESUtils.addCSS('#progressIndicator .gearIcon { margin-left: 1em; }');
RESUtils.addCSS('#NREMailCount { margin-left: 0; float: left; margin-top: 3px;}');
RESUtils.addCSS('#NREPause { margin-left: 2px; width: 16px; height: 16px; float: left; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); cursor: pointer; }');
RESUtils.addCSS('#NREPause, #NREPause.paused.reversePause { background-position: -16px -192px; }');
RESUtils.addCSS('#NREPause.paused, #NREPause.reversePause { background-position: 0 -192px; }');
}
},
go: function() {
if ((this.isEnabled()) && (this.isMatchURL())) {
/* if (RESUtils.pageType() !== 'linklist') {
sessionStorage.NERpageURL = location.href;
}
*/ // modified from a contribution by Peter Siewert, thanks Peter!
if (typeof modules['neverEndingReddit'].dupeHash === 'undefined') modules['neverEndingReddit'].dupeHash = {};
var entries = document.body.querySelectorAll('a.comments');
for (var i = entries.length - 1; i > -1; i--) {
modules['neverEndingReddit'].dupeHash[entries[i].href] = 1;
}
this.allLinks = document.body.querySelectorAll('#siteTable div.thing');
// code inspired by River of Reddit, but rewritten from scratch to work across multiple browsers...
// Original River of Reddit author: reddy kapil
// Original link to Chrome extension: https://chrome.google.com/extensions/detail/bjiggjllfebckflfdjbimogjieeghcpp
// store access to the siteTable div since that's where we'll append new data...
var stMultiCheck = document.querySelectorAll('#siteTable');
this.siteTable = stMultiCheck[0];
// stupid sponsored links create a second div with ID of sitetable (bad reddit! you should never have 2 IDs with the same name! naughty, naughty reddit!)
if (stMultiCheck.length === 2) {
// console.log('skipped first sitetable, stupid reddit.');
this.siteTable = stMultiCheck[1];
}
// get the first link to the next page of reddit...
var nextPrevLinks = document.body.querySelectorAll('.content .nextprev a');
if (nextPrevLinks.length > 0) {
var nextLink = nextPrevLinks[nextPrevLinks.length-1];
if (nextLink) {
this.nextPageURL = nextLink.getAttribute('href');
var nextXY=RESUtils.getXYpos(nextLink);
this.nextPageScrollY = nextXY.y;
}
this.attachLoaderWidget();
//Reset this info if the page is in a new tab
// wait, this is always tre... commenting out.
/*
if (window.history.length) {
console.log('delete nerpage');
delete sessionStorage['NERpage'];
*/
if (this.options.returnToPrevPage.value) {
// if the user clicks any external links, save that link
// get all external links and track clicks...
/* $('body').on('click', 'a.title[href^="http://"]', function(e) {
// if left click and not going to open in a new tab...
if ((this.target !== '_blank') && (e.which === 1)) sessionStorage.lastPageURL = this.href;
});
*/
this.returnToPrevPageCheck(location.hash);
}
// watch for the user scrolling to the bottom of the page. If they do it, load a new page.
if (this.options.autoLoad.value) {
window.addEventListener('scroll', modules['neverEndingReddit'].handleScroll, false);
}
}
// check if the user has new mail...
this.navMail = document.body.querySelector('#mail');
this.NREFloat = createElementWithID('div','NREFloat');
this.NREPause = createElementWithID('div','NREPause');
this.NREPause.setAttribute('title','Pause / Restart Never Ending Reddit');
if (this.options.reversePauseIcon.value) this.NREPause.classList.add('reversePause');
this.isPaused = (RESStorage.getItem('RESmodules.neverEndingReddit.isPaused') == true);
if (this.isPaused) this.NREPause.classList.add('paused');
this.NREPause.addEventListener('click',modules['neverEndingReddit'].togglePause, false);
if ((modules['betteReddit'].options.pinHeader.value !== 'userbar') && (modules['betteReddit'].options.pinHeader.value !== 'header')) {
this.NREMail = createElementWithID('a','NREMail');
if (modules['betteReddit'].options.pinHeader.value === 'sub') {
RESUtils.addCSS('#NREFloat { position: fixed; top: 23px; right: 8px; display: none; }');
} else if (modules['betteReddit'].options.pinHeader.value === 'subanduser') {
RESUtils.addCSS('#NREFloat { position: fixed; top: 44px; right: 0; display: none; }');
RESUtils.addCSS('#NREMail { display: none; }');
RESUtils.addCSS('#NREMailCount { display: none; }');
} else {
RESUtils.addCSS('#NREFloat { position: fixed; top: 10px; right: 10px; display: none; }');
}
RESUtils.addCSS('#NREMail { width: 16px; height: 12px; float: left; margin-top: 4px; background: center center no-repeat; }');
RESUtils.addCSS('#NREMail.nohavemail { background-image: url(http://www.redditstatic.com/mailgray.png); }');
RESUtils.addCSS('#NREMail.havemail { background-image: url(http://www.redditstatic.com/mail.png); }');
RESUtils.addCSS('.res-colorblind #NREMail.havemail { background-image: url(http://thumbs.reddit.com/t5_2s10b_5.png); }');
this.NREFloat.appendChild(this.NREMail);
this.NREMailCount = createElementWithID('a','NREMailCount');
this.NREMailCount.display = 'none';
this.NREMailCount.setAttribute('href',modules['betteReddit'].getInboxLink(true));
this.NREFloat.appendChild(this.NREMailCount);
var hasNew = false;
if ((typeof this.navMail !== 'undefined') && (this.navMail !== null)) {
hasNew = this.navMail.classList.contains('havemail');
}
this.setMailIcon(hasNew);
} else {
this.NREMail = this.navMail;
RESUtils.addCSS('#NREFloat { position: fixed; top: 30px; right: 8px; display: none; }');
}
this.NREFloat.appendChild(this.NREPause);
document.body.appendChild(this.NREFloat);
}
},
pageMarkers: [],
pageURLs: [],
togglePause: function() {
modules['neverEndingReddit'].isPaused = !modules['neverEndingReddit'].isPaused;
RESStorage.setItem('RESmodules.neverEndingReddit.isPaused', modules['neverEndingReddit'].isPaused);
if (modules['neverEndingReddit'].isPaused) {
modules['neverEndingReddit'].NREPause.classList.add('paused');
if (modules['neverEndingReddit'].options.notifyWhenPaused.value) {
var notification = [];
notification.push('Never-Ending Reddit has been paused. Click the play/pause button to unpause it.');
notification.push('To hide this message, disable Never-Ending Reddit\'s ' + modules['settingsNavigation'].makeUrlHashLink('neverEndingReddit', 'notifyWhenPaused', 'notifyWhenPaused option ') + '.');
notification = notification.join('
');
RESUtils.notification({
moduleID: 'neverEndingReddit',
message: notification
});
}
} else {
modules['neverEndingReddit'].NREPause.classList.remove('paused');
modules['neverEndingReddit'].handleScroll();
}
modules['neverEndingReddit'].setWidgetActionText();
},
returnToPrevPageCheck: function(hash) {
var pageRE = /page=(\d+)/,
match = pageRE.exec(hash);
// Set the current page to page 1...
this.currPage = 1;
if (match) {
var backButtonPageNumber = match[1] || 1;
if (backButtonPageNumber > 1) {
this.attachModalWidget();
this.currPage = backButtonPageNumber;
this.loadNewPage(true);
}
}
/*
if ((sessionStorage.NERpageURL) && (sessionStorage.NERpageURL != sessionStorage.lastPageURL)) {
var backButtonPageNumber = sessionStorage.getItem('NERpage') || 1;
if (backButtonPageNumber > 1) {
this.currPage = backButtonPageNumber;
this.loadNewPage(true);
}
}
sessionStorage.lastPageURL = location.href;
*/
},
handleScroll: function(e) {
if (modules['neverEndingReddit'].scrollTimer) clearTimeout(modules['neverEndingReddit'].scrollTimer);
modules['neverEndingReddit'].scrollTimer = setTimeout(modules['neverEndingReddit'].handleScrollAfterTimer, 300);
},
handleScrollAfterTimer: function(e) {
var thisPageNum = 1,
thisMarker;
for (var i=0, len=modules['neverEndingReddit'].pageMarkers.length; i 1) {
// sessionStorage.NERpageURL = location.href;
sessionStorage.NERpage = thisPageNum;
modules['neverEndingReddit'].pastFirstPage = true;
location.hash = 'page='+thisPageNum;
} else {
if (location.hash.indexOf('page=') !== -1) {
location.hash = 'page='+thisPageNum;
}
delete sessionStorage['NERpage'];
}
}
if ((modules['neverEndingReddit'].fromBackButton != true) && (modules['neverEndingReddit'].options.returnToPrevPage.value)) {
for (var i=0, len=modules['neverEndingReddit'].allLinks.length; i 30) {
modules['neverEndingReddit'].showFloat(true);
} else {
modules['neverEndingReddit'].showFloat(false);
}
},
pauseAfterPages: null,
pauseAfter: function(currPageNum) {
if (this.pauseAfterPages === null) {
this.pauseAfterPages = parseInt(modules['neverEndingReddit'].options.pauseAfterEvery.value);
}
if ((this.pauseAfterPages > 0) && (currPageNum % this.pauseAfterPages === 0)) {
this.togglePause(true);
var notification = [];
notification.push('Time for a break!');
notification.push('Never-Ending Reddit was paused automatically. ' + modules['settingsNavigation'].makeUrlHashLink('neverEndingReddit', 'pauseAfterEvery', '', 'gearIcon'));
notification = notification.join('
');
setTimeout(RESUtils.notification.bind(RESUtils, notification, 5000));
}
},
duplicateCheck: function(newHTML){
var newLinks = newHTML.querySelectorAll('div.link');
for(var i = newLinks.length - 1; i > -1; i--) {
var newLink = newLinks[i];
var thisCommentLink = newLink.querySelector('a.comments').href;
if( modules['neverEndingReddit'].dupeHash[thisCommentLink] ) {
// let's not remove it altogether, but instead dim it...
// newLink.parentElement.removeChild(newLink);
newLink.classList.add('NERdupe');
} else {
modules['neverEndingReddit'].dupeHash[thisCommentLink] = 1;
}
}
return newHTML;
},
setMailIcon: function(newmail) {
if (RESUtils.loggedInUser() === null) return false;
if (newmail) {
modules['neverEndingReddit'].hasNewMail = true;
this.NREMail.classList.remove('nohavemail');
this.NREMail.setAttribute('href', modules['betteReddit'].getInboxLink(true));
this.NREMail.setAttribute('title','new mail!');
this.NREMail.classList.add('havemail');
modules['betteReddit'].showUnreadCount();
} else {
modules['neverEndingReddit'].hasNewMail = false;
this.NREMail.classList.add('nohavemail');
this.NREMail.setAttribute('href',modules['betteReddit'].getInboxLink(false));
this.NREMail.setAttribute('title','no new mail');
this.NREMail.classList.remove('havemail');
modules['betteReddit'].setUnreadCount(0);
}
},
attachModalWidget: function() {
this.modalWidget = createElementWithID('div','NERModal');
$(this.modalWidget).html(' ');
this.modalContent = createElementWithID('div','NERContent');
$(this.modalContent).html('
×
Never Ending Reddit has detected that you are returning from a page that it loaded. Please give us a moment while we reload that content and return you to where you left off. ');
document.body.appendChild(this.modalWidget);
document.body.appendChild(this.modalContent);
$('#NERModalClose').click(function() {
$(modules['neverEndingReddit'].modalWidget).hide();
$(modules['neverEndingReddit'].modalContent).hide();
});
},
attachLoaderWidget: function() {
// add a widget at the bottom that will be used to detect that we've scrolled to the bottom, and will also serve as a "loading" bar...
this.progressIndicator = document.createElement('div');
this.setWidgetActionText();
this.progressIndicator.id = 'progressIndicator';
this.progressIndicator.className = 'neverEndingReddit';
this.progressIndicator.addEventListener('click', function(e) {
if (e.target.id !== 'NERStaticLink' && !e.target.classList.contains('gearIcon')) {
e.preventDefault();
modules['neverEndingReddit'].loadNewPage();
}
}, false);
insertAfter(this.siteTable, this.progressIndicator);
},
setWidgetActionText: function () {
$(this.progressIndicator).empty();
$('
Never Ending Reddit
')
.appendTo(this.progressIndicator)
.append(modules['settingsNavigation'].makeUrlHashLink('neverEndingReddit', null, ' ', 'gearIcon'));
var text = "Click to load the next page";
if (this.options.autoLoad.value && !this.isPaused) {
text = "scroll or click to load the next page";
} else if (this.options.autoLoad.value && this.isPaused) {
text = "click to load the next page; or click the 'pause' button in the top right corner"
}
$('')
.text(text)
.appendTo(this.progressIndicator);
var nextpage = $('or open next page')
.attr('href', this.nextPageURL);
$('').append(nextpage)
.append(' (and clear Never-Ending stream)')
.appendTo(this.progressIndicator);
},
loadNewPage: function(fromBackButton, reload) {
var me = modules['neverEndingReddit'];
if (me.isLoading != true) {
me.isLoading = true;
if (fromBackButton) {
me.fromBackButton = true;
var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
var savePageURL = me.nextPageURL;
me.nextPageURL = RESStorage.getItem('RESmodules.neverEndingReddit.lastPage.'+thisPageType);
if ((me.nextPageURL === 'undefined') || (me.nextPageURL == null)) {
// something went wrong, probably someone hit refresh. Just revert to the first page...
modules['neverEndingReddit'].fromBackButton = false;
me.nextPageURL = savePageURL;
me.currPage = 1;
me.isLoading = false;
return false;
}
var leftCentered = Math.floor((window.innerWidth - 720) / 2);
me.modalWidget.style.display = 'block';
me.modalContent.style.display = 'block';
me.modalContent.style.left = leftCentered + 'px';
// remove the progress indicator early, as we don't want the user to scroll past it on accident, loading more content.
me.progressIndicator.parentNode.removeChild(modules['neverEndingReddit'].progressIndicator);
} else {
me.fromBackButton = false;
}
me.progressIndicator.removeEventListener('click', modules['neverEndingReddit'].loadNewPage , false);
$(me.progressIndicator).html(' Loading next page...');
// as a sanity check, which should NEVER register true, we'll make sure me.nextPageURL is on the same domain we're browsing...
if (me.nextPageURL.indexOf(location.hostname) === -1) {
console.log('Next page URL mismatch. Something strange may be afoot.')
me.isLoading = false;
return false;
}
GM_xmlhttpRequest({
method: "GET",
url: me.nextPageURL,
onload: function(response) {
if ((typeof modules['neverEndingReddit'].progressIndicator.parentNode !== 'undefined') && (modules['neverEndingReddit'].progressIndicator.parentNode !== null)) {
modules['neverEndingReddit'].progressIndicator.parentNode.removeChild(modules['neverEndingReddit'].progressIndicator);
}
// drop the HTML we got back into a div...
var thisHTML = response.responseText;
var tempDiv = document.createElement('div');
// clear out any javascript so we don't render it again...
$(tempDiv).html(thisHTML.replace(/
'); macroGroup.container.append(macroGroup.dropdown); } return macroGroup; }, addButtonToMacroGroup: function(groupName, button) { var group = this.getMacroGroup(groupName); group.dropdown.append($("