2 // @name Reddit Enhancement Suite
3 // @namespace http://reddit.honestbleeps.com/
4 // @description A suite of tools to enhance reddit...
5 // @copyright 2010-2013, Steve Sobel (http://redditenhancementsuite.com/)
6 // @license GPL version 3 or any later version; http://www.gnu.org/copyleft/gpl.html
7 // @author honestbleeps
8 // @include http://redditenhancementsuite.com/*
9 // @include http://reddit.honestbleeps.com/*
10 // @include http://reddit.com/*
11 // @include https://reddit.com/*
12 // @include http://*.reddit.com/*
13 // @include https://*.reddit.com/*
15 // @updateURL http://redditenhancementsuite.com/latest/reddit_enhancement_suite.meta.js
16 // @downloadURL http://redditenhancementsuite.com/latest/reddit_enhancement_suite.user.js
17 // @require https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js
20 /*jshint undef: true, unused: true, strict: false, laxbreak: true, multistr: true, smarttabs: true, sub: true, browser: true */
22 var RESVersion = "4.3.0.4";
24 var jQuery, $, guiders, Tinycon, SnuOwnd;
27 Reddit Enhancement Suite - a suite of tools to enhance Reddit
28 Copyright (C) 2010-2012 - honestbleeps (steve@honestbleeps.com)
30 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):
32 Because RES auto updates and is hosted from a central server, I humbly request that if you intend to distribute your own
33 modified Reddit Enhancement Suite, you name it something else and make it very clear to your users that it's your own
34 branch and isn't related to mine.
36 RES is updated very frequently, and I get lots of tech support questions/requests from people on outdated versions. If
37 you're distributing RES via your own means, those recipients won't always be on the latest and greatest, which makes
38 it harder for me to debug things and understand (at least with browsers that auto-update) whether or not people are on
39 a current version of RES.
41 I can't legally hold you to any of this - I'm just asking out of courtesy.
43 Thanks, I appreciate your consideration. Without further ado, the all-important GPL Statement:
45 This program is free software: you can redistribute it and/or modify
46 it under the terms of the GNU General Public License as published by
47 the Free Software Foundation, either version 3 of the License, or
48 (at your option) any later version.
50 This program is distributed in the hope that it will be useful,
51 but WITHOUT ANY WARRANTY; without even the implied warranty of
52 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
53 GNU General Public License for more details.
55 You should have received a copy of the GNU General Public License
56 along with this program. If not, see <http://www.gnu.org/licenses/>.
60 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; }';
61 tokenizeCSS += '.optionsTable ul.token-input-list-facebook {clear: left; float: none; margin-right: 0; }';
62 tokenizeCSS += 'ul.token-input-list-facebook li input { border: 0; width: 100px; padding: 3px 8px; background-color: white; margin: 2px 0; -webkit-appearance: caret; }';
63 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; }';
64 tokenizeCSS += 'li.token-input-token-facebook p { display: inline; padding: 0; margin: 0;}';
65 tokenizeCSS += 'li.token-input-token-facebook span { color: #a6b3cf; margin-left: 5px; font-weight: bold; cursor: pointer;}';
66 tokenizeCSS += 'li.token-input-selected-token-facebook { background-color: #5670a6; border: 1px solid #3b5998; color: #fff;}';
67 tokenizeCSS += 'li.token-input-input-token-facebook { float: left; margin: 0; padding: 0; list-style-type: none;}';
68 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; }';
69 tokenizeCSS += 'div.token-input-dropdown-facebook p { margin: 0; padding: 5px; font-weight: bold; color: #777;}';
70 tokenizeCSS += 'div.token-input-dropdown-facebook ul { margin: 0; padding: 0;}';
71 tokenizeCSS += 'div.token-input-dropdown-facebook ul li { background-color: #fff; padding: 3px; margin: 0; list-style-type: none;}';
72 tokenizeCSS += 'div.token-input-dropdown-facebook ul li.token-input-dropdown-item-facebook { background-color: #fff;}';
73 tokenizeCSS += 'div.token-input-dropdown-facebook ul li.token-input-dropdown-item2-facebook { background-color: #fff;}';
74 tokenizeCSS += 'div.token-input-dropdown-facebook ul li em { font-weight: bold; font-style: normal;}';
75 tokenizeCSS += 'div.token-input-dropdown-facebook ul li.token-input-selected-dropdown-item-facebook { background-color: #3b5998; color: #fff;}';
78 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;}';
79 guidersCSS += '.guider_buttons { height: 36px; position: relative; width: 100%; }';
80 guidersCSS += '.guider_content { position: relative; }';
81 guidersCSS += '.guider_description { margin-bottom: 10px; }';
82 guidersCSS += '.guider_content h1 { color: #1054AA; float: left; font-size: 21px; }';
83 guidersCSS += '.guider_close { float: right; padding: 10px 0 0; }';
84 // guidersCSS += '.x_button { background-image: url(\'x_close_button.jpg\'); cursor: pointer; height: 13px; width: 13px; }';
85 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; }';
86 guidersCSS += '.guider_content p { clear: both; color: #333; font-size: 13px; }';
87 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; }';
88 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; }';
90 * For optimization, the arrows image is inlined in the css below.
92 * To use your own arrows image, replace this background-image with your own arrows.
93 * It should have four arrows, top, right, left, and down.
95 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); } ';
96 guidersCSS += '.guider_arrow_right { display: block; background-position: 0 0; right: -42px; }';
97 guidersCSS += '.guider_arrowdown { display: block; background-position: 0 -42px; bottom: -42px; }';
98 guidersCSS += '.guider_arrow_up { display: block; background-position: 0 -126px; top: -42px; }';
99 guidersCSS += '.guider_arrow_left { display: block; background-position: 0 -84px; left: -42px;}';
100 guidersCSS += '.guider_content h2 { margin-top: 1em; }';
101 guidersCSS += '.guider_content td { vertical-align: top; }';
102 guidersCSS += '.guider_content td + td { padding-left: 1em; }';
103 guidersCSS += '.guider_content td code { white-space: nowrap; }';
107 // DOM utility functions
108 var escapeLookups = { "&": "&", '"': """, "<": "<", ">": ">" };
109 function escapeHTML(str) {
110 return (typeof str === 'undefined' || str === null) ?
112 str.toString().replace(/[&"<>]/g, function(m) { return escapeLookups[m]; });
115 function insertAfter( referenceNode, newNode ) {
116 if ((typeof referenceNode === 'undefined') || (referenceNode === null)) {
117 console.log(arguments.callee.caller);
118 } else if ((typeof referenceNode.parentNode !== 'undefined') && (typeof referenceNode.nextSibling !== 'undefined')) {
119 if (referenceNode.parentNode === null) {
120 console.log(arguments.callee.caller);
122 referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling );
126 function createElementWithID(elementType, id, classname) {
127 var obj = document.createElement(elementType);
129 obj.setAttribute('id', id);
131 if ((typeof classname !== 'undefined') && (classname !== '')) {
132 obj.setAttribute('class', classname);
137 // this alias is to account for opera having different behavior...
138 if (typeof navigator === 'undefined') navigator = window.navigator;
140 //Because Safari 5.1 doesn't have Function.bind
141 if (typeof Function.prototype.bind === 'undefined') {
142 Function.prototype.bind = function(context) {
145 return oldRef.apply(context || null, Array.prototype.slice.call(arguments));
150 var BrowserDetect = {
152 this.browser = this.searchString(this.dataBrowser) || "An unknown browser";
153 this.version = this.searchVersion(navigator.userAgent) ||
154 this.searchVersion(navigator.appVersion) ||
155 "an unknown version";
156 this.OS = this.searchString(this.dataOS) || "an unknown OS";
158 // set up MutationObserver variable to take whichever is supported / existing...
159 // unfortunately, this doesn't (currently) exist in Opera.
160 // this.MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver || null;
161 // At the time of writing WebKit's mutation observer leaks entire pages on refresh so it needs to be disabled.
162 this.MutationObserver = window.MutationObserver || window.WebKitMutationObserver || window.MozMutationObserver || null;
164 // null out MutationObserver to test legacy DOMNodeInserted
165 // this.MutationObserver = null;
167 searchString: function (data) {
168 for (var i=0;i<data.length;i++) {
169 var dataString = data[i].string;
170 var dataProp = data[i].prop;
171 this.versionSearchString = data[i].versionSearch || data[i].identity;
173 if (dataString.indexOf(data[i].subString) !== -1)
174 return data[i].identity;
177 return data[i].identity;
180 searchVersion: function (dataString) {
181 var index = dataString.indexOf(this.versionSearchString);
182 if (index === -1) return;
183 return parseFloat(dataString.substring(index+this.versionSearchString.length+1));
185 isChrome: function() { return typeof chrome !== 'undefined'; },
186 isFirefox: function() { return typeof self.on === 'function'; },
187 isOperaBlink: function() { return typeof chrome !== 'undefined' && BrowserDetect.browser === "Opera"; },
188 isOpera: function() { return typeof opera !== 'undefined'; },
189 isSafari: function() { return typeof safari !== 'undefined'; },
192 string: navigator.userAgent,
197 string: navigator.userAgent,
201 { string: navigator.userAgent,
202 subString: "OmniWeb",
203 versionSearch: "OmniWeb/",
207 string: navigator.vendor,
210 versionSearch: "Version"
215 versionSearch: "Version"
218 string: navigator.vendor,
223 string: navigator.vendor,
225 identity: "Konqueror"
228 string: navigator.userAgent,
229 subString: "Firefox",
233 string: navigator.vendor,
237 { // for newer Netscapes (6+)
238 string: navigator.userAgent,
239 subString: "Netscape",
243 string: navigator.userAgent,
245 identity: "Explorer",
246 versionSearch: "MSIE"
249 string: navigator.userAgent,
255 // for older Netscapes (4-)
256 string: navigator.userAgent,
257 subString: "Mozilla",
258 identity: "Netscape",
259 versionSearch: "Mozilla"
264 string: navigator.platform,
269 string: navigator.platform,
274 string: navigator.userAgent,
276 identity: "iPhone/iPod"
279 string: navigator.platform,
286 BrowserDetect.init();
289 // safely parses JSON and won't kill the whole script if JSON.parse fails
290 // if localStorageSource is specified, will offer the user the ability to delete that localStorageSource to stop further errors.
291 // if silent is specified, it will fail silently...
292 parse: function(data, localStorageSource, silent) {
294 if (BrowserDetect.isSafari()) {
295 if (data.substring(0,2) === 's{') {
296 data = data.substring(1,data.length);
299 return JSON.parse(data);
301 if (silent) return {};
302 if (localStorageSource) {
303 var msg = 'Error caught: JSON parse failure on the following data from "'+localStorageSource+'": <textarea rows="5" cols="50">' + data + '</textarea><br>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.';
304 alert(msg, function() {
305 // back up a copy of the corrupt data
306 localStorage.setItem(localStorageSource + '.error', data);
307 // delete the corrupt data
308 RESStorage.removeItem(localStorageSource);
311 alert('Error caught: JSON parse failure on the following data: ' + data);
318 // array compare utility function for keyCode arrays
319 function keyArrayCompare(fromArr, toArr) {
320 // if we've passed in a number, fix that and make it an array with alt, shift and ctrl set to false.
321 if (typeof toArr === 'number') {
322 toArr = [toArr, false, false, false];
323 } else if (toArr.length === 4) {
326 if (fromArr.length !== toArr.length) return false;
327 for (var i = 0; i < toArr.length; i++) {
328 if (fromArr[i].compare) {
329 if (!fromArr[i].compare(toArr[i])) return false;
331 if (fromArr[i] !== toArr[i]) return false;
336 // utility function for checking events against keyCode arrays
337 function checkKeysForEvent(event, keyArray) {
338 //[keycode, alt, ctrl, shift, meta]
339 // if we've passed in a number, fix that and make it an array with alt, shift and ctrl set to false.
340 if (typeof keyArray === 'number') {
341 keyArray = [keyArray, false, false, false, false];
342 } else if (keyArray.length === 4) {
343 keyArray.push(false);
345 if (event.keyCode != keyArray[0]) return false;
346 else if (event.altKey != keyArray[1]) return false;
347 else if (event.ctrlKey != keyArray[2]) return false;
348 else if (event.shiftKey != keyArray[3]) return false;
349 else if (event.metaKey != keyArray[4]) return false;
353 function operaUpdateCallback(obj) {
354 RESUtils.compareVersion(obj);
356 function operaForcedUpdateCallback(obj) {
357 RESUtils.compareVersion(obj, true);
360 // This object will store xmlHTTPRequest callbacks for Safari because Safari's extension architecture seems stupid.
361 // 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...
362 xhrQueue = { count: 0, onloads: [] };
365 // if this is a jetpack addon, add an event listener like Safari's message handler...
366 if (BrowserDetect.isFirefox()) {
367 self.on('message', function(msgEvent) {
368 switch (msgEvent.name) {
369 case 'GM_xmlhttpRequest':
370 // Fire the appropriate onload function for this xmlhttprequest.
371 xhrQueue.onloads[msgEvent.XHRID](msgEvent.response);
373 case 'compareVersion':
374 var forceUpdate = false;
375 if (typeof msgEvent.message.forceUpdate !== 'undefined') forceUpdate = true;
376 RESUtils.compareVersion(msgEvent.message, forceUpdate);
379 var tweet = msgEvent.response;
380 var thisExpando = modules['styleTweaks'].tweetExpando;
381 $(thisExpando).html(tweet.html);
382 thisExpando.style.display = 'block';
383 thisExpando.classList.add('twitterLoaded');
385 // for now, commenting out the old way of handling tweets as AMO will not approve.
387 var tweet = msgEvent.response;
388 var thisExpando = modules['styleTweaks'].tweetExpando;
389 thisExpando.innerHTML = '';
390 // the iframe is to sandbox this remote javascript from accessing reddit's javascript, etc.
391 // this is done this way as requested by the AMO review team.
392 var sandboxFrame = document.createElement('iframe');
393 var seamless = document.createAttribute('seamless');
394 sandboxFrame.setAttribute('sandbox','allow-scripts allow-same-origin');
395 sandboxFrame.setAttributeNode(seamless);
396 sandboxFrame.setAttribute('style','border: none;');
397 sandboxFrame.setAttribute('width','480');
398 sandboxFrame.setAttribute('height','260');
399 sandboxFrame.setAttribute('src','data:text/html,<html><head><base href="https://platform.twitter.com"></head><body>'+encodeURIComponent(tweet.html)+"</body></html>");
400 $(thisExpando).append(sandboxFrame);
401 // $(thisExpando).html(tweet.html);
402 thisExpando.style.display = 'block';
403 thisExpando.classList.add('twitterLoaded');
405 case 'getLocalStorage':
406 // Does RESStorage have actual data in it? If it doesn't, they're a legacy user, we need to copy
407 // old school localStorage from the foreground page to the background page to keep their settings...
408 if (typeof msgEvent.message.importedFromForeground === 'undefined') {
409 // it doesn't exist.. copy it over...
411 requestType: 'saveLocalStorage',
414 self.postMessage(thisJSON);
416 setUpRESStorage(msgEvent.message);
420 case 'saveLocalStorage':
421 // Okay, we just copied localStorage from foreground to background, let's set it up...
422 setUpRESStorage(msgEvent.message);
425 RESStorage.setItem(msgEvent.itemName, msgEvent.itemValue, true);
428 // console.log('unknown event type in self.on');
429 // console.log(msgEvent.toSource());
435 // This is the message handler for Safari - the background page calls this function with return data...
436 function safariMessageHandler(msgEvent) {
437 switch (msgEvent.name) {
438 case 'GM_xmlhttpRequest':
439 // Fire the appropriate onload function for this xmlhttprequest.
440 xhrQueue.onloads[msgEvent.message.XHRID](msgEvent.message);
442 case 'compareVersion':
443 var forceUpdate = false;
444 if (typeof msgEvent.message.forceUpdate !== 'undefined') forceUpdate = true;
445 RESUtils.compareVersion(msgEvent.message, forceUpdate);
448 var tweet = msgEvent.message;
449 var thisExpando = modules['styleTweaks'].tweetExpando;
450 $(thisExpando).html(tweet.html);
451 thisExpando.style.display = 'block';
452 thisExpando.classList.add('twitterLoaded');
454 case 'getLocalStorage':
455 // Does RESStorage have actual data in it? If it doesn't, they're a legacy user, we need to copy
456 // old schol localStorage from the foreground page to the background page to keep their settings...
457 if (typeof msgEvent.message.importedFromForeground === 'undefined') {
458 // it doesn't exist.. copy it over...
460 requestType: 'saveLocalStorage',
463 safari.self.tab.dispatchMessage('saveLocalStorage', thisJSON);
465 setUpRESStorage(msgEvent.message);
469 case 'saveLocalStorage':
470 // Okay, we just copied localStorage from foreground to background, let's set it up...
471 setUpRESStorage(msgEvent.message);
474 case 'addURLToHistory':
475 var url = msgEvent.message.url;
476 modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
479 RESStorage.setItem(msgEvent.message.itemName, msgEvent.message.itemValue, true);
482 // console.log('unknown event type in safariMessageHandler');
487 // This is the message handler for Opera - the background page calls this function with return data...
488 function operaMessageHandler(msgEvent) {
489 var eventData = msgEvent.data;
490 switch (eventData.msgType) {
491 case 'GM_xmlhttpRequest':
492 // Fire the appropriate onload function for this xmlhttprequest.
493 xhrQueue.onloads[eventData.XHRID](eventData.data);
495 case 'compareVersion':
496 var forceUpdate = false;
497 if (typeof eventData.data.forceUpdate !== 'undefined') forceUpdate = true;
498 RESUtils.compareVersion(eventData.data, forceUpdate);
501 var tweet = eventData.data;
502 var thisExpando = modules['styleTweaks'].tweetExpando;
503 $(thisExpando).html(tweet.html);
504 thisExpando.style.display = 'block';
505 thisExpando.classList.add('twitterLoaded');
507 case 'getLocalStorage':
508 // Does RESStorage have actual data in it? If it doesn't, they're a legacy user, we need to copy
509 // old schol localStorage from the foreground page to the background page to keep their settings...
510 if (typeof eventData.data.importedFromForeground === 'undefined') {
511 // it doesn't exist.. copy it over...
513 requestType: 'saveLocalStorage',
516 opera.extension.postMessage(JSON.stringify(thisJSON));
518 if (location.hostname.match('reddit')) {
519 setUpRESStorage(eventData.data);
524 case 'saveLocalStorage':
525 // Okay, we just copied localStorage from foreground to background, let's set it up...
526 setUpRESStorage(eventData.data);
527 if (location.hostname.match('reddit')) {
532 if ((typeof RESStorage !== 'undefined') && (typeof RESStorage.setItem === 'function')) {
533 RESStorage.setItem(eventData.itemName, eventData.itemValue, true);
535 // a change in opera requires this wait/timeout for the RESStorage grab to work...
536 var waitForRESStorage = function(eData) {
537 if ((typeof RESStorage !== 'undefined') && (typeof RESStorage.setItem === 'function')) {
538 RESStorage.setItem(eData.itemName, eData.itemValue, true);
540 setTimeout(function() { waitForRESStorage(eData); }, 200);
543 var savedEventData = {
544 itemName: eventData.itemName,
545 itemValue: eventData.itemValue
547 waitForRESStorage(savedEventData);
550 case 'addURLToHistory':
551 var url = eventData.url;
552 if (! eventData.isPrivate) {
553 modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
557 // console.log('unknown event type in operaMessageHandler');
562 // listen for messages from chrome background page
563 if (BrowserDetect.isChrome()) {
564 chrome.extension.onMessage.addListener(
565 function(request, sender, sendResponse) {
566 switch(request.requestType) {
568 RESStorage.setItem(request.itemName, request.itemValue, true);
571 // sendResponse({status: "unrecognized request type"});
578 if (BrowserDetect.isSafari()) {
579 // Safari has a ridiculous bug that causes it to lose access to safari.self.tab if you click the back button.
580 // this stupid one liner fixes that.
581 window.onunload = function(){};
582 safari.self.addEventListener("message", safariMessageHandler, false);
584 // we can't do this check for opera here because we need to wait until DOMContentLoaded is triggered, I think. Putting this in RESinit();
586 // opera compatibility
587 if (BrowserDetect.isOpera()) {
588 // removing this line for new localStorage methodology (store in extension localstorage)
589 sessionStorage = window.sessionStorage;
590 localStorage = window.localStorage;
591 location = window.location;
592 XMLHttpRequest = window.XMLHttpRequest;
595 // 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.
596 if (typeof unsafeWindow !== 'undefined') {
597 if ((typeof unsafeWindow.console !== 'undefined') && (!BrowserDetect.isFirefox())) {
598 console = unsafeWindow.console;
599 } else if (typeof console === 'undefined') {
610 // GreaseMonkey API compatibility for non-GM browsers (Chrome, Safari, Firefox)
611 // @copyright 2009, 2010 James Campos
612 // @modified 2010 Steve Sobel - added some missing gm_* functions
613 // @license cc-by-3.0; http://creativecommons.org/licenses/by/3.0/
614 if ((typeof GM_deleteValue === 'undefined') || (typeof GM_addStyle === 'undefined')) {
615 GM_addStyle = function(css) {
616 var style = document.createElement('style');
617 style.textContent = css;
618 var head = document.getElementsByTagName('head')[0];
620 head.appendChild(style);
624 GM_deleteValue = function(name) {
625 localStorage.removeItem(name);
628 GM_getValue = function(name, defaultValue) {
629 var value = localStorage.getItem(name);
633 value = value.substring(1);
636 return value === 'true';
638 return Number(value);
644 GM_log = function(message) {
645 console.log(message);
648 GM_registerMenuCommand = function(name, funk) {
652 GM_setValue = function(name, value) {
653 value = (typeof value)[0] + value;
654 localStorage.setItem(name, value);
657 if (BrowserDetect.browser === "Explorer") {
658 GM_xmlhttpRequest = function(obj) {
660 crossDomain = (obj.url.indexOf(location.hostname) === -1);
661 if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
662 obj.requestType = 'GM_xmlhttpRequest';
663 request = new XDomainRequest();
664 request.onload = function() {obj.onload(request);};
665 request.onerror = function() {if (obj.onerror) {obj.onerror(request);}};
666 request.open(obj.method,obj.url);
667 request.send(obj.data);
670 request = new XMLHttpRequest();
671 request.onreadystatechange=function() {
672 if (obj.onreadystatechange) {
673 obj.onreadystatechange(request);
675 if (request.readyState === 4 && obj.onload) {
679 request.onerror = function() {
681 obj.onerror(request);
685 request.open(obj.method,obj.url,true);
694 statusText:'Forbidden'
700 for (var name in obj.headers) {
701 request.setRequestHeader(name,obj.headers[name]);
704 request.send(obj.data);
709 if (BrowserDetect.isChrome()) {
710 GM_xmlhttpRequest = function(obj) {
711 var crossDomain = (obj.url.indexOf(location.hostname) === -1);
713 if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
714 obj.requestType = 'GM_xmlhttpRequest';
715 if (typeof obj.onload !== 'undefined') {
716 chrome.extension.sendMessage(obj, function(response) {
717 obj.onload(response);
721 var request=new XMLHttpRequest();
722 request.onreadystatechange = function() {
723 if (obj.onreadystatechange) {
724 obj.onreadystatechange(request);
726 if(request.readyState === 4 && obj.onload) {
730 request.onerror = function() {
732 obj.onerror(request);
736 request.open(obj.method,obj.url,true);
745 statusText:'Forbidden'
750 if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
751 request.send(obj.data); return request;
754 } else if (BrowserDetect.isSafari()) {
755 GM_xmlhttpRequest = function(obj) {
756 obj.requestType = 'GM_xmlhttpRequest';
757 // Since Safari doesn't provide legitimate callbacks, I have to store the onload function here in the main
758 // userscript in a queue (see xhrQueue), wait for data to come back from the background page, then call the onload.
760 // oy vey... another problem. When Safari sends xmlhttpRequests from the background page, it loses the cookies etc that it'd have
761 // had from the foreground page... so we need to write a bit of a hack here, and call different functions based on whether or
762 // not the request is cross domain... For same-domain requests, we'll call from the foreground...
763 var crossDomain = (obj.url.indexOf(location.hostname) === -1);
765 if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
766 obj.XHRID = xhrQueue.count;
767 xhrQueue.onloads[xhrQueue.count] = obj.onload;
768 safari.self.tab.dispatchMessage("GM_xmlhttpRequest", obj);
771 var request=new XMLHttpRequest();
772 request.onreadystatechange = function() {
773 if (obj.onreadystatechange) {
774 obj.onreadystatechange(request);
776 if (request.readyState === 4 && obj.onload) {
780 request.onerror = function() {
782 obj.onerror(request);
786 request.open(obj.method,obj.url,true);
795 statusText:'Forbidden'
800 if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
801 request.send(obj.data); return request;
804 } else if (BrowserDetect.isOpera()) {
805 GM_xmlhttpRequest = function(obj) {
806 obj.requestType = 'GM_xmlhttpRequest';
807 // Turns out, Opera works this way too, but I'll forgive them since their extensions are so young and they're awesome people...
809 // oy vey... cross domain same issue with Opera.
810 var crossDomain = (obj.url.indexOf(location.hostname) === -1);
812 if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
813 obj.XHRID = xhrQueue.count;
814 xhrQueue.onloads[xhrQueue.count] = obj.onload;
815 opera.extension.postMessage(JSON.stringify(obj));
818 var request=new XMLHttpRequest();
819 request.onreadystatechange = function() {
820 if (obj.onreadystatechange) {
821 obj.onreadystatechange(request);
823 if (request.readyState === 4 && obj.onload) {
827 request.onerror = function() {
829 obj.onerror(request);
833 request.open(obj.method,obj.url,true);
842 statusText:'Forbidden'
848 for (var name in obj.headers) {
849 request.setRequestHeader(name,obj.headers[name]);
852 request.send(obj.data); return request;
855 } else if (BrowserDetect.isFirefox()) {
856 // we must be in a Firefox / jetpack addon...
857 GM_xmlhttpRequest = function(obj) {
858 var crossDomain = (obj.url.indexOf(location.hostname) === -1);
860 if ((typeof obj.onload !== 'undefined') && (crossDomain)) {
861 obj.requestType = 'GM_xmlhttpRequest';
862 // okay, firefox's jetpack addon does this same stuff... le sigh..
863 if (typeof obj.onload !== 'undefined') {
864 obj.XHRID = xhrQueue.count;
865 xhrQueue.onloads[xhrQueue.count] = obj.onload;
866 self.postMessage(obj);
870 var request=new XMLHttpRequest();
871 request.onreadystatechange = function() {
872 if (obj.onreadystatechange) {
873 obj.onreadystatechange(request);
875 if(request.readyState === 4 && obj.onload) {
879 request.onerror = function() {
881 obj.onerror(request);
885 request.open(obj.method,obj.url,true);
894 statusText:'Forbidden'
899 if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
900 request.send(obj.data); return request;
905 // 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.
906 // yes, it's ugly, but it's necessary if we're using Greasemonkey together with jQuery this way.
907 var oldgmx = GM_xmlhttpRequest;
908 GM_xmlhttpRequest = function(params) {
909 setTimeout(function() {
918 // define common RESUtils - reddit related functions and data that may need to be accessed...
920 preInit: function() {
921 // we store a localStorage key because the async call is too slow to add classes to
922 // the document prior to page load, thus the flash of unstyled content.
923 RESUtils.getDocHTML();
925 // to avoid the flash of unstyled content, the very first thing we should do is get a hold
926 // of the document object and add necessary classes...
927 getDocHTML: function() {
929 document.html = document.documentElement;
930 if (localStorage.getItem('RES_nightMode')) {
931 // no need to check the background - we're in night mode for sure.
932 modules['styleTweaks'].redditDark();
935 setTimeout(RESUtils.getDocHTML, 1);
938 // A cache variable to store CSS that will be applied at the end of execution...
939 randomHash: function(len) {
940 var chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXTZabcdefghiklmnopqrstuvwxyz";
941 var numChars = len || 5;
942 var randomString = '';
943 for (var i=0; i<numChars; i++) {
944 var rnum = Math.floor(Math.random() * chars.length);
945 randomString += chars.substring(rnum,rnum+1);
951 addCSS: function(css) {
952 if (RESUtils.postLoad) {
953 var style = $('<style />').html(css).appendTo('head');
955 remove: function() { style.remove(); }
961 insertParam: function(href, key, value) {
963 if (href.indexOf('?') === -1) pre = '?';
964 return href + pre + key + '=' + value;
966 // checks if script should run on current URL using exclude / include.
967 isMatchURL: function (moduleID) {
969 var currURL = location.href;
970 // get includes and excludes...
971 var excludes = modules[moduleID].exclude;
972 var includes = modules[moduleID].include;
973 // first check excludes...
974 if (typeof excludes !== 'undefined') {
975 for (i=0, len = excludes.length; i<len; i++) {
976 // console.log(moduleID + ' -- ' + excludes[i] + ' - excl test - ' + currURL + ' - result: ' + excludes[i].test(currURL));
977 if (excludes[i].test(currURL)) {
982 // then check includes...
983 for (i=0, len=includes.length; i<len; i++) {
984 // console.log(moduleID + ' -- ' + includes[i] + ' - incl test - ' + currURL + ' - result: ' + includes[i].test(currURL));
985 if (includes[i].test(currURL)) {
991 // gets options for a module...
992 getOptionsFirstRun: [],
993 getOptions: function(moduleID) {
994 if (this.getOptionsFirstRun[moduleID]) {
995 // we've already grabbed these out of localstorage, so modifications should be done in memory. just return that object.
996 return modules[moduleID].options;
998 var thisOptions = RESStorage.getItem('RESoptions.' + moduleID);
999 if ((thisOptions) && (thisOptions !== 'undefined') && (thisOptions !== null)) {
1000 // merge options (in case new ones were added via code) and if anything has changed, update to localStorage
1001 var storedOptions = safeJSON.parse(thisOptions, 'RESoptions.' + moduleID);
1002 var codeOptions = modules[moduleID].options;
1003 var newOption = false;
1004 for (var attrname in codeOptions) {
1005 if (typeof storedOptions[attrname] === 'undefined') {
1007 storedOptions[attrname] = codeOptions[attrname];
1009 codeOptions[attrname].value = storedOptions[attrname].value;
1012 modules[moduleID].options = codeOptions;
1014 RESStorage.setItem('RESoptions.' + moduleID, JSON.stringify(modules[moduleID].options));
1017 // nothing in localStorage, let's set the defaults...
1018 RESStorage.setItem('RESoptions.' + moduleID, JSON.stringify(modules[moduleID].options));
1020 this.getOptionsFirstRun[moduleID] = true;
1021 return modules[moduleID].options;
1023 getUrlParams: function () {
1024 var result = {}, queryString = location.search.substring(1),
1025 re = /([^&=]+)=([^&]*)/g, m;
1026 while ((m = re.exec(queryString))) {
1027 result[decodeURIComponent(m[1])] = decodeURIComponent(m[2]);
1031 setOption: function(moduleID, optionName, optionValue) {
1032 if (optionName.match(/_[\d]+$/)) {
1033 optionName = optionName.replace(/_[\d]+$/,'');
1035 var thisOptions = this.getOptions(moduleID);
1036 var saveOptionValue;
1037 if (optionValue === '') {
1038 saveOptionValue = '';
1039 } else if ((isNaN(optionValue)) || (typeof optionValue === 'boolean') || (typeof optionValue === 'object')) {
1040 saveOptionValue = optionValue;
1041 } else if (optionValue.indexOf('.') !== -1) {
1042 saveOptionValue = parseFloat(optionValue);
1044 saveOptionValue = parseInt(optionValue, 10);
1046 thisOptions[optionName].value = saveOptionValue;
1047 // save it to the object...
1048 modules[moduleID].options = thisOptions;
1049 // save it to RESStorage...
1050 RESStorage.setItem('RESoptions.' + moduleID, JSON.stringify(modules[moduleID].options));
1053 click: function(obj, button) {
1054 var evt = document.createEvent('MouseEvents');
1055 button = button || 0;
1056 evt.initMouseEvent('click', true, true, window.wrappedJSObject, 0, 1, 1, 1, 1, false, false, false, false, button, null);
1057 obj.dispatchEvent(evt);
1059 mousedown: function(obj, button) {
1060 var evt = document.createEvent('MouseEvents');
1061 button = button || 0;
1062 evt.initMouseEvent('mousedown', true, true, window.wrappedJSObject, 0, 1, 1, 1, 1, false, false, false, false, button, null);
1063 obj.dispatchEvent(evt);
1065 loggedInUser: function(tryingEarly) {
1066 if (typeof this.loggedInUserCached === 'undefined') {
1067 var userLink = document.querySelector('#header-bottom-right > span.user > a');
1068 if ((userLink !== null) && (!userLink.classList.contains('login-required'))) {
1069 this.loggedInUserCached = userLink.innerHTML;
1070 this.loggedInUserHashCached = document.querySelector('[name=uh]').value;
1073 // trying early means we're trying before DOM load may be complete, so if we fail here
1074 // we don't want to null this, we want to allow another try.
1075 // currently the only place this is really used is username hider, which tries (if possible)
1076 // to hide the username as early/fast as possible.
1077 delete this.loggedInUserCached;
1078 delete this.loggedInUserHashCached;
1080 this.loggedInUserCached = null;
1084 return this.loggedInUserCached;
1086 loggedInUserHash: function() {
1087 this.loggedInUser();
1088 return this.loggedInUserHashCached;
1090 getUserInfo: function(callback, username, live) {
1091 // Default to currently logged-in user, for backwards compatibility
1092 username = (typeof username !== "undefined" ? username : RESUtils.loggedInUser());
1093 if (username === null) return false;
1095 // Default to getting live data (i.e. from reddit's server)
1096 live = (typeof live === "boolean" ? live : true);
1098 if (!(username in RESUtils.userInfoCallbacks)) {
1099 RESUtils.userInfoCallbacks[username] = [];
1101 RESUtils.userInfoCallbacks[username].push(callback);
1102 var cacheData = RESStorage.getItem('RESUtils.userInfoCache.' + username) || '{}';
1103 var userInfoCache = safeJSON.parse(cacheData);
1104 var lastCheck = (userInfoCache !== null) ? parseInt(userInfoCache.lastCheck, 10) || 0 : 0;
1105 var now = new Date();
1106 // 300000 = 5 minutes
1107 if (live && (now.getTime() - lastCheck) > 300000) {
1108 if (!RESUtils.userInfoRunning) {
1109 RESUtils.userInfoRunning = true;
1112 url: location.protocol + "//" + location.hostname + "/user/" + encodeURIComponent(username) + "/about.json?app=res",
1113 onload: function(response) {
1114 var thisResponse = JSON.parse(response.responseText);
1115 var userInfoCache = {
1116 lastCheck: now.getTime(),
1117 userInfo: thisResponse
1119 RESStorage.setItem('RESUtils.userInfoCache.' + username, JSON.stringify(userInfoCache));
1120 while (RESUtils.userInfoCallbacks[username].length > 0) {
1121 var thisCallback = RESUtils.userInfoCallbacks[username].pop();
1122 thisCallback(userInfoCache.userInfo);
1124 RESUtils.userInfoRunning = false;
1129 while (RESUtils.userInfoCallbacks[username].length > 0) {
1130 var thisCallback = RESUtils.userInfoCallbacks[username].pop();
1131 thisCallback(userInfoCache.userInfo);
1135 userInfoCallbacks: {},
1136 commentsRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*comments\/?[-\w\.\/]*/i,
1137 friendsCommentsRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/r\/friends\/*comments\/?/i,
1138 inboxRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/message\/[-\w\.\/]*/i,
1139 profileRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.#=]*\/?(comments)?\/?(\?([a-z]+=[a-zA-Z0-9_%]*&?)*)?$/i, // fix to regex contributed by s_quark
1140 submitRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/([-\w\.\/]*\/)?submit\/?(\?.*)?$/i,
1141 prefsRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/prefs\/?/i,
1142 wikiRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\.]+\/wiki?/i,
1143 pageType: function() {
1144 if (typeof this.pageTypeSaved === 'undefined') {
1146 var currURL = location.href.split('#')[0];
1147 if (RESUtils.profileRegex.test(currURL)) {
1148 pageType = 'profile';
1149 } else if ((RESUtils.commentsRegex.test(currURL)) || (RESUtils.friendsCommentsRegex.test(currURL))) {
1150 pageType = 'comments';
1151 } else if (RESUtils.inboxRegex.test(currURL)) {
1153 } else if (RESUtils.submitRegex.test(currURL)) {
1154 pageType = 'submit';
1155 } else if (RESUtils.prefsRegex.test(currURL)) {
1157 } else if (RESUtils.wikiRegex.test(currURL)) {
1160 pageType = 'linklist';
1162 this.pageTypeSaved = pageType;
1164 return this.pageTypeSaved;
1166 commentPermalinkRegex: /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*comments\/[a-z0-9]+\/[^\/]+\/[a-z0-9]+\/?$/i,
1167 isCommentPermalinkPage: function() {
1168 if (typeof this.isCommentPermalinkSaved === 'undefined') {
1169 var currURL = location.href.split('#')[0];
1170 if (RESUtils.commentPermalinkRegex.test(currURL)) {
1171 this.isCommentPermalinkSaved = true;
1173 this.isCommentPermalinkSaved = false;
1177 return this.isCommentPermalinkSaved;
1179 matchRE: /^https?:\/\/(?:[a-z]+)\.reddit\.com\/r\/([\w\.\+]+).*/i,
1180 matchDOM: /^https?:\/\/(?:[a-z]+)\.reddit\.com\/domain\/([\w\.\+]+).*/i,
1181 currentSubreddit: function(check) {
1182 if (typeof this.curSub === 'undefined') {
1183 var match = location.href.match(RESUtils.matchRE);
1184 if (match !== null) {
1185 this.curSub = match[1];
1186 if (check) return (match[1].toLowerCase() === check.toLowerCase());
1189 if (check) return false;
1193 if (check) return (this.curSub.toLowerCase() === check.toLowerCase());
1197 currentDomain: function(check) {
1198 if (typeof this.curDom === 'undefined') {
1199 var match = location.href.match(RESUtils.matchDOM);
1200 if (match !== null) {
1201 this.curDom = match[1];
1202 if (check) return (match[1].toLowerCase() === check.toLowerCase());
1205 if (check) return false;
1209 if (check) return (this.curDom.toLowerCase() === check.toLowerCase());
1213 currentUserProfile: function() {
1214 if (typeof this.curUserProfile === 'undefined') {
1215 var match = location.href.match(/^https?:\/\/(?:[a-z]+)\.reddit\.com\/user\/([\w\.]+).*/i);
1216 if (match !== null) {
1217 this.curUserProfile = match[1];
1223 return this.curUserProfile;
1226 getXYpos: function (obj) {
1227 var topValue= 0,leftValue= 0;
1229 leftValue += obj.offsetLeft;
1230 topValue += obj.offsetTop;
1231 obj = obj.offsetParent;
1233 return { 'x': leftValue, 'y': topValue };
1235 elementInViewport: function (obj) {
1236 // 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.
1237 var headerOffset = this.getHeaderOffset();
1238 var top = obj.offsetTop - headerOffset;
1239 var left = obj.offsetLeft;
1240 var width = obj.offsetWidth;
1241 var height = obj.offsetHeight;
1242 while(obj.offsetParent) {
1243 obj = obj.offsetParent;
1244 top += obj.offsetTop;
1245 left += obj.offsetLeft;
1248 top >= window.pageYOffset &&
1249 left >= window.pageXOffset &&
1250 (top + height) <= (window.pageYOffset + window.innerHeight - headerOffset) &&
1251 (left + width) <= (window.pageXOffset + window.innerWidth)
1254 setMouseXY: function(e) {
1255 e = e || window.event;
1256 var cursor = {x:0, y:0};
1257 if (e.pageX || e.pageY) {
1261 cursor.x = e.clientX +
1262 (document.documentElement.scrollLeft ||
1263 document.body.scrollLeft) -
1264 document.documentElement.clientLeft;
1265 cursor.y = e.clientY +
1266 (document.documentElement.scrollTop ||
1267 document.body.scrollTop) -
1268 document.documentElement.clientTop;
1270 RESUtils.mouseX = cursor.x;
1271 RESUtils.mouseY = cursor.y;
1273 elementUnderMouse: function ( obj ) {
1275 top = $obj.offset().top,
1276 left = $obj.offset().left,
1277 width = $obj.outerWidth(),
1278 height = $obj.outerHeight(),
1279 right = left + width,
1280 bottom = top + height;
1281 if ((RESUtils.mouseX >= left) && (RESUtils.mouseX <= right) && (RESUtils.mouseY >= top) && (RESUtils.mouseY <= bottom)) {
1287 doElementsCollide: function (ele1, ele2, margin) {
1288 margin = margin || 0;
1292 var dims1 = ele1.offset();
1293 dims1.right = dims1.left + ele1.width();
1294 dims1.bottom = dims1.top + ele1.height();
1296 dims1.left -= margin;
1297 dims1.top -= margin;
1298 dims1.right += margin;
1299 dims1.bottom += margin;
1302 var dims2 = ele2.offset();
1303 dims2.right = dims2.left + ele2.width();
1304 dims2.bottom = dims2.top + ele2.height();
1308 (dims1.left < dims2.left && dims2.left < dims1.right) ||
1309 (dims1.left < dims2.right && dims2.right < dims1.right) ||
1310 (dims2.left < dims1.left && dims1.left < dims2.right) ||
1311 (dims2.left < dims1.right && dims1.right < dims2.right)
1314 (dims1.top < dims2.top && dims2.top < dims1.bottom) ||
1315 (dims1.top < dims2.bottom && dims2.bottom < dims1.bottom) ||
1316 (dims2.top < dims1.top && dims1.top < dims2.bottom) ||
1317 (dims2.top < dims1.bottom && dims1.bottom < dims2.bottom))
1320 // In layman's terms:
1321 // If one of the box's left/right borders is between the other box's left/right
1322 // and same with top/bottom,
1323 // then they collide.
1324 // This could probably be logicked into a more compact form.
1331 scrollTo: function(x,y) {
1332 var headerOffset = this.getHeaderOffset();
1333 window.scrollTo(x,y-headerOffset);
1335 getHeaderOffset: function() {
1336 if (typeof this.headerOffset === 'undefined') {
1337 this.headerOffset = 0;
1338 switch (modules['betteReddit'].options.pinHeader.value) {
1342 this.theHeader = document.querySelector('#sr-header-area');
1345 this.theHeader = document.querySelector('#sr-header-area');
1348 this.theHeader = document.querySelector('#header');
1351 if (this.theHeader) {
1352 this.headerOffset = this.theHeader.offsetHeight + 6;
1355 return this.headerOffset;
1357 setSelectValue: function(obj, value) {
1358 for (var i=0, len=obj.length; i < len; i++) {
1359 // for some reason in firefox, obj[0] is undefined... weird. adding a test for existence of obj[i]...
1360 // okay, now as of ff8, it's even barfing here unless we console.log out a check - nonsensical.
1361 // a bug has been filed to bugzilla at:
1362 // https://bugzilla.mozilla.org/show_bug.cgi?id=702847
1363 if ((obj[i]) && (obj[i].value == value)) {
1364 obj[i].selected = true;
1368 stripHTML: function(str) {
1369 var regExp = /<\/?[^>]+>/gi;
1370 str = str.replace(regExp, "");
1373 sanitizeHTML: function(htmlStr) {
1374 if (!this.sanitizer) {
1375 var SnuOwnd = window.SnuOwnd;
1376 var redditCallbacks = SnuOwnd.getRedditCallbacks();
1377 var callbacks = SnuOwnd.createCustomCallbacks({
1378 paragraph: function(out, text, options){
1379 if (text) out.s += text.s;
1381 autolink: redditCallbacks.autolink,
1382 raw_html_tag: redditCallbacks.raw_html_tag
1384 var rendererConfig = SnuOwnd.defaultRenderState();
1385 rendererConfig.flags = SnuOwnd.DEFAULT_WIKI_FLAGS;
1386 rendererConfig.html_element_whitelist = [
1387 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'span', 'div', 'code',
1388 'br', 'hr', 'p', 'a', 'img', 'pre', 'blockquote', 'table',
1389 'thead', 'tbody', 'tfoot', 'tr', 'th', 'td', 'strong', 'em',
1390 'i', 'b', 'u', 'ul', 'ol', 'li', 'dl', 'dt', 'dd',
1391 'font', 'center', 'small', 's', 'q', 'sub', 'sup', 'del'
1393 rendererConfig.html_attr_whitelist = [
1394 'href', 'title', 'src', 'alt', 'colspan',
1395 'rowspan', 'cellspacing', 'cellpadding', 'scope',
1396 'face', 'color', 'size', 'bgcolor', 'align'
1398 this.sanitizer = SnuOwnd.getParser({
1399 callbacks: callbacks,
1400 context: rendererConfig
1403 return this.sanitizer.render(htmlStr);
1405 fadeElementOut: function(obj, speed, callback) {
1406 if (obj.getAttribute('isfading') === 'in') {
1409 obj.setAttribute('isfading','out');
1410 speed = speed || 0.1;
1411 if (obj.style.opacity === '') obj.style.opacity = '1';
1412 if (obj.style.opacity <= 0) {
1413 obj.style.display = 'none';
1414 obj.setAttribute('isfading',false);
1415 if (callback) callback();
1418 var newOpacity = parseFloat(obj.style.opacity) - speed;
1419 if (newOpacity < speed) newOpacity = 0;
1420 obj.style.opacity = newOpacity;
1421 setTimeout(function() { RESUtils.fadeElementOut(obj, speed, callback); }, 100);
1424 fadeElementIn: function(obj, speed, finalOpacity) {
1425 finalOpacity = finalOpacity || 1;
1426 if (obj.getAttribute('isfading') === 'out') {
1429 obj.setAttribute('isfading','in');
1430 speed = speed || 0.1;
1431 if ((obj.style.display === 'none') || (obj.style.display === '')) {
1432 obj.style.opacity = 0;
1433 obj.style.display = 'block';
1435 if (obj.style.opacity >= finalOpacity) {
1436 obj.setAttribute('isfading',false);
1437 obj.style.opacity = finalOpacity;
1440 var newOpacity = parseFloat(obj.style.opacity) + parseFloat(speed);
1441 if (newOpacity > finalOpacity) newOpacity = finalOpacity;
1442 obj.style.opacity = newOpacity;
1443 setTimeout(function() { RESUtils.fadeElementIn(obj, speed, finalOpacity); }, 100);
1446 setCursorPosition: function(form, pos) {
1450 if (elem.setSelectionRange) {
1451 elem.setSelectionRange(pos, pos);
1452 } else if (elem.createTextRange) {
1453 var range = elem.createTextRange();
1454 range.collapse(true);
1455 range.moveEnd('character', pos);
1456 range.moveStart('character', pos);
1462 setNewNotification: function() {
1463 $('#RESSettingsButton, #RESMainGearOverlay .gearIcon').addClass('newNotification').click(function() {
1464 location.href = '/r/RESAnnouncements';
1467 createMultiLock: function() {
1472 lock: function(lockname, value) {
1473 if (typeof lockname === "undefined") return;
1474 if (locks[lockname]) return;
1476 locks[lockname] = value || true;
1480 unlock: function(lockname) {
1481 if (typeof lockname === "undefined") return;
1482 if (!locks[lockname]) return;
1484 locks[lockname] = false;
1488 locked: function(lockname) {
1489 if (typeof lockname !== "undefined") {
1490 // Is this lock set?
1491 return locks[lockname];
1499 indexOptionTable: function(moduleID, optionKey, keyFieldIndex) {
1500 var source = modules[moduleID].options[optionKey].value;
1502 modules[moduleID].options[optionKey].fields[keyFieldIndex].type === 'list' ?
1505 return RESUtils.indexArrayByProperty(source, keyFieldIndex, keyIsList);
1507 indexArrayByProperty: function(source, keyIndex, keyValueSeparator) {
1508 if (!source || !source.length) {
1509 return function() { };
1512 var index = createIndex();
1515 function createIndex() {
1518 for (var i = 0, length = source.length; i < length; i++) {
1519 var item = source[i];
1520 var key = item && item[keyIndex];
1523 if (keyValueSeparator) {
1524 var keys = key.toLowerCase().split(keyValueSeparator);
1525 for (var ki = 0, klength = keys.length; ki < klength; ki++) {
1537 function getItem(key) {
1538 key = key && key.toLowerCase();
1539 var item = index[key];
1543 inList: function(needle, haystack, separator, isCaseSensitive) {
1544 if (!needle || !haystack) return false;
1546 separator = separator || ',';
1548 if (haystack.indexOf(separator) !== -1) {
1549 var haystacks = haystack.split(separator);
1550 if (RESUtils.inArray(needle, haystacks, isCaseSensitive)) {
1554 if (caseSensitive) {
1555 return (needle == haystack);
1557 return (needle.toLowerCase() == haystack.toLowerCase());
1561 inArray: function(needle, haystacks, isCaseSensitive) {
1562 if (!isCaseSensitive) needle = needle.toLowerCase();
1564 for (var i = 0, length = haystacks.length; i < length; i++) {
1565 if (isCaseSensitive) {
1566 if (needle == haystacks[i]) {
1570 if (needle == haystacks[i].toLowerCase()) {
1576 firstRun: function() {
1577 // if this is the first time this version has been run, pop open the what's new tab, background focused.
1578 if (RESStorage.getItem('RES.firstRun.'+RESVersion) === null) {
1579 RESStorage.setItem('RES.firstRun.'+RESVersion,'true');
1580 RESUtils.openLinkInNewTab('http://redditenhancementsuite.com/whatsnew.html?v='+RESVersion, false);
1583 // checkForUpdate: function(forceUpdate) {
1584 checkForUpdate: function() {
1585 if (RESUtils.currentSubreddit('RESAnnouncements')) {
1586 RESStorage.removeItem('RES.newAnnouncement','true');
1588 var now = new Date();
1589 var lastCheck = parseInt(RESStorage.getItem('RESLastUpdateCheck'), 10) || 0;
1590 // if we haven't checked for an update in 24 hours, check for one now!
1591 // if (((now.getTime() - lastCheck) > 86400000) || (RESVersion > RESStorage.getItem('RESlatestVersion')) || ((RESStorage.getItem('RESoutdated') === 'true') && (RESVersion == RESStorage.getItem('RESlatestVersion'))) || forceUpdate) {
1592 if ((now.getTime() - lastCheck) > 86400000) {
1593 // now we're just going to check /r/RESAnnouncements for new posts, we're not checking version numbers...
1594 var lastID = RESStorage.getItem('RES.lastAnnouncementID');
1595 $.getJSON('/r/RESAnnouncements/.json?limit=1&app=res', function(data) {
1596 RESStorage.setItem('RESLastUpdateCheck',now.getTime());
1597 var thisID = data.data.children[0].data.id;
1598 if (thisID != lastID) {
1599 RESStorage.setItem('RES.newAnnouncement','true');
1600 RESUtils.setNewNotification();
1602 RESStorage.setItem('RES.lastAnnouncementID', thisID);
1605 var jsonURL = 'http://reddit.honestbleeps.com/update.json?v=' + RESVersion;
1606 // mark off that we've checked for an update...
1607 RESStorage.setItem('RESLastUpdateCheck',now.getTime());
1608 var outdated = false;
1609 if (BrowserDetect.isChrome()) {
1610 // we've got chrome, so we need to hit up the background page to do cross domain XHR
1612 requestType: 'compareVersion',
1615 chrome.extension.sendMessage(thisJSON, function(response) {
1616 // send message to background.html to open new tabs...
1617 outdated = RESUtils.compareVersion(response, forceUpdate);
1619 } else if (BrowserDetect.isSafari()) {
1620 // we've got safari, so we need to hit up the background page to do cross domain XHR
1622 requestType: 'compareVersion',
1624 forceUpdate: forceUpdate
1626 safari.self.tab.dispatchMessage("compareVersion", thisJSON);
1627 } else if (BrowserDetect.isOpera()) {
1628 // we've got opera, so we need to hit up the background page to do cross domain XHR
1630 requestType: 'compareVersion',
1632 forceUpdate: forceUpdate
1634 opera.extension.postMessage(JSON.stringify(thisJSON));
1636 // we've got greasemonkey, so we can do cross domain XHR.
1640 onload: function(response) {
1641 outdated = RESUtils.compareVersion(JSON.parse(response.responseText), forceUpdate);
1649 compareVersion: function(response, forceUpdate) {
1650 if (RESVersion < response.latestVersion) {
1651 RESStorage.setItem('RESoutdated','true');
1652 RESStorage.setItem('RESlatestVersion',response.latestVersion);
1653 RESStorage.setItem('RESmessage',response.message);
1655 $(RESConsole.RESCheckUpdateButton).html('You are out of date! <a target="_blank" href="http://reddit.honestbleeps.com/download">[click to update]</a>');
1659 RESStorage.setItem('RESlatestVersion',response.latestVersion);
1660 RESStorage.setItem('RESoutdated','false');
1662 $(RESConsole.RESCheckUpdateButton).html('You are up to date!');
1668 proEnabled: function() {
1669 return ((typeof modules['RESPro'] !== 'undefined') && (modules['RESPro'].isEnabled()));
1671 niceKeyCode: function(charCode) {
1672 var keyComboString = '';
1673 var testCode, niceString;
1674 if (typeof charCode === 'string') {
1675 var tempArray = charCode.split(',');
1676 if (tempArray.length) {
1677 if (tempArray[1] === 'true') keyComboString += 'alt-';
1678 if (tempArray[2] === 'true') keyComboString += 'ctrl-';
1679 if (tempArray[3] === 'true') keyComboString += 'shift-';
1680 if (tempArray[4] === 'true') keyComboString += 'command-';
1682 testCode = parseInt(charCode, 10);
1683 } else if (typeof charCode === 'object') {
1684 testCode = parseInt(charCode[0], 10);
1685 if (charCode[1]) keyComboString += 'alt-';
1686 if (charCode[2]) keyComboString += 'ctrl-';
1687 if (charCode[3]) keyComboString += 'shift-';
1688 if (charCode[4]) keyComboString += 'command-';
1692 niceString = "backspace"; // backspace
1695 niceString = "tab"; // tab
1698 niceString = "enter"; // enter
1701 niceString = "shift"; // shift
1704 niceString = "ctrl"; // ctrl
1707 niceString = "alt"; // alt
1710 niceString = "pause/break"; // pause/break
1713 niceString = "caps lock"; // caps lock
1716 niceString = "escape"; // escape
1719 niceString = "page up"; // page up, to avoid displaying alternate character and confusing people
1722 niceString = "page down"; // page down
1725 niceString = "end"; // end
1728 niceString = "home"; // home
1731 niceString = "left arrow"; // left arrow
1734 niceString = "up arrow"; // up arrow
1737 niceString = "right arrow"; // right arrow
1740 niceString = "down arrow"; // down arrow
1743 niceString = "insert"; // insert
1746 niceString = "delete"; // delete
1749 niceString = "left window"; // left window
1752 niceString = "right window"; // right window
1755 niceString = "select key"; // select key
1758 niceString = "numpad 0"; // numpad 0
1761 niceString = "numpad 1"; // numpad 1
1764 niceString = "numpad 2"; // numpad 2
1767 niceString = "numpad 3"; // numpad 3
1770 niceString = "numpad 4"; // numpad 4
1773 niceString = "numpad 5"; // numpad 5
1776 niceString = "numpad 6"; // numpad 6
1779 niceString = "numpad 7"; // numpad 7
1782 niceString = "numpad 8"; // numpad 8
1785 niceString = "numpad 9"; // numpad 9
1788 niceString = "multiply"; // multiply
1791 niceString = "add"; // add
1794 niceString = "subtract"; // subtract
1797 niceString = "decimal point"; // decimal point
1800 niceString = "divide"; // divide
1803 niceString = "F1"; // F1
1806 niceString = "F2"; // F2
1809 niceString = "F3"; // F3
1812 niceString = "F4"; // F4
1815 niceString = "F5"; // F5
1818 niceString = "F6"; // F6
1821 niceString = "F7"; // F7
1824 niceString = "F8"; // F8
1827 niceString = "F9"; // F9
1830 niceString = "F10"; // F10
1833 niceString = "F11"; // F11
1836 niceString = "F12"; // F12
1839 niceString = "num lock"; // num lock
1842 niceString = "scroll lock"; // scroll lock
1845 niceString = ";"; // semi-colon
1848 niceString = "="; // equal-sign
1851 niceString = ","; // comma
1854 niceString = "-"; // dash
1857 niceString = "."; // period
1860 niceString = "/"; // forward slash
1863 niceString = "`"; // grave accent
1866 niceString = "["; // open bracket
1869 niceString = "\\"; // back slash
1872 niceString = "]"; // close bracket
1875 niceString = "'"; // single quote
1878 niceString = String.fromCharCode(testCode);
1881 return keyComboString + niceString;
1883 niceDate: function(d, usformat) {
1884 d = d || new Date();
1885 var year = d.getFullYear();
1886 var month = (d.getMonth() + 1);
1887 month = (month < 10) ? '0'+month : month;
1888 var day = d.getDate();
1889 day = (day < 10) ? '0'+day : day;
1890 var fullString = year+'-'+month+'-'+day;
1892 fullString = month+'-'+day+'-'+year;
1896 niceDateTime: function(d, usformat) {
1897 d = d || new Date();
1898 var dateString = RESUtils.niceDate(d);
1899 var hours = d.getHours();
1900 hours = (hours < 10) ? '0'+hours : hours;
1901 var minutes = d.getMinutes();
1902 minutes = (minutes < 10) ? '0'+minutes : minutes;
1903 var seconds = d.getSeconds();
1904 seconds = (seconds < 10) ? '0'+seconds : seconds;
1905 var fullString = dateString + ' ' + hours + ':'+minutes+':'+seconds;
1908 niceDateDiff: function(origdate, newdate) {
1909 // Enter the month, day, and year below you want to use as
1910 // the starting point for the date calculation
1912 newdate = new Date();
1915 var amonth = origdate.getUTCMonth() + 1;
1916 var aday = origdate.getUTCDate();
1917 var ayear = origdate.getUTCFullYear();
1919 var tyear = newdate.getUTCFullYear();
1920 var tmonth = newdate.getUTCMonth() + 1;
1921 var tday = newdate.getUTCDate();
1930 if (((tyear % 4 === 0) && (tyear % 100 !== 0)) || (tyear % 400 === 0)) {
1934 var m = [31, f, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1936 var dyear = tyear - ayear;
1938 var dmonth = tmonth - amonth;
1939 if (dmonth < 0 && dyear > 0) {
1940 dmonth = dmonth + 12;
1944 var dday = tday - aday;
1947 var ma = amonth + tmonth;
1949 if (ma >= 12) { ma = ma - 12; }
1950 if (ma < 0) { ma = ma + 12; }
1951 dday = dday + m[ma];
1955 dmonth = dmonth + 12;
1962 var returnString = '';
1964 if (dyear === 0) { y = 0; }
1965 if (dmonth === 0) { mm = 0; }
1966 if (dday === 0) { d = 0; }
1967 if ((y === 1) && (mm === 1)) { a1 = 1; }
1968 if ((y === 1) && (d === 1)) { a1 = 1; }
1969 if ((mm === 1) && (d === 1)) { a2 = 1; }
1972 returnString += dyear + " year";
1974 returnString += dyear + " years";
1977 if ((a1 === 1) && (a2 === 0)) { returnString += " and "; }
1978 if ((a1 === 1) && (a2 === 1)) { returnString += ", "; }
1981 returnString += dmonth + " month";
1983 returnString += dmonth + " months";
1986 if (a2 === 1) { returnString += " and "; }
1989 returnString += dday + " day";
1991 returnString += dday + " days";
1994 if (returnString === '') {
1995 returnString = '0 days';
1997 return returnString;
1999 checkIfSubmitting: function() {
2000 this.checkedIfSubmitting = true;
2001 if ((location.href.match(/\/r\/[\w]+\/submit\/?/i)) || (location.href.match(/reddit\.com\/submit\/?/i))) {
2002 var thisSubRedditInput = document.getElementById('sr-autocomplete');
2003 if (thisSubRedditInput) {
2004 var thisSubReddit = thisSubRedditInput.value;
2005 var title = document.querySelector('textarea[name=title]');
2006 if (typeof this.thisSubRedditInputListener === 'undefined') {
2007 this.thisSubRedditInputListener = true;
2008 thisSubRedditInput.addEventListener('change', function(e) {
2009 RESUtils.checkIfSubmitting();
2012 if ((thisSubReddit.toLowerCase() === 'enhancement') || (thisSubReddit.toLowerCase() === 'resissues')) {
2013 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; }');
2014 RESUtils.addCSS('.submittingToEnhancementButton { border: 1px solid #444; border-radius: 2px; padding: 3px 6px; cursor: pointer; display: inline-block; margin-top: 12px; }');
2015 RESUtils.addCSS('#RESBugReport, #RESFeatureRequest { display: none; }');
2016 RESUtils.addCSS('#RESSubmitOptions .submittingToEnhancementButton { margin-top: 30px; }');
2017 var textDesc = document.getElementById('text-desc');
2018 this.submittingToEnhancement = createElementWithID('div','submittingToEnhancement','RESDialogSmall');
2019 /*jshint multistr: true */
2020 var submittingHTML = " \
2021 <h3>Submitting to r/Enhancement</h3> \
2022 <div class=\"RESDialogContents\"> \
2023 <div id=\"RESSubmitOptions\"> \
2024 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!<br> \
2025 <div id=\"RESSubmitBug\" class=\"submittingToEnhancementButton\">I want to submit a bug report</div><br> \
2026 <div id=\"RESSubmitFeatureRequest\" class=\"submittingToEnhancementButton\">I want to submit a feature request</div><br> \
2027 <div id=\"RESSubmitOther\" class=\"submittingToEnhancementButton\">I want to submit a general question or other item</div> \
2029 <div id=\"RESBugReport\"> \
2030 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: <br> \
2032 <li>Have you searched /r/RESIssues to see if someone else has reported it?</li> \
2033 <li>Have you checked the <a target=\"_blank\" href=\"http://www.reddit.com/r/Enhancement/wiki/faq\">RES FAQ?</a></li> \
2034 <li>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?</li> \
2037 Please also check out the latest known / popular bugs first:<br> \
2038 <ul id=\"RESKnownBugs\"><li style=\"color: red;\">Loading...</li></ul> \
2039 <span id=\"submittingBug\" class=\"submittingToEnhancementButton\">I still want to submit a bug!</span> \
2041 <div id=\"RESFeatureRequest\"> \
2042 So you want to request a feature, great! Please just consider the following, first:<br> \
2044 <li>Have you searched /r/Enhancement to see if someone else has requested it?</li> \
2045 <li>Is it something that would appeal to Reddit as a whole? Personal or subreddit specific requests usually aren't added to RES.</li> \
2048 Please also check out the latest known popular feature requests first:<br> \
2049 <ul id=\"RESKnownFeatureRequests\"><li style=\"color: red;\">Loading...</li></ul> \
2050 <span id=\"submittingFeature\" class=\"submittingToEnhancementButton\">I still want to submit a feature request!<span> \
2053 $(this.submittingToEnhancement).html(submittingHTML);
2054 insertAfter(textDesc, this.submittingToEnhancement);
2055 setTimeout(function() {
2056 $('#RESSubmitBug').click(
2058 $('#RESSubmitOptions').fadeOut(
2060 $('#RESBugReport').fadeIn();
2063 url: 'http://redditenhancementsuite.com/knownbugs.json',
2064 onload: function(response) {
2065 $('#RESKnownBugs').html('');
2066 var data = safeJSON.parse(response.responseText);
2067 $.each(data, function(key, val) {
2068 $('#RESKnownBugs').append('<li><a target="_blank" href="'+val.url+'">'+val.description+'</a></li>');
2076 $('#RESSubmitFeatureRequest').click(
2078 $('#RESSubmitOptions').fadeOut(
2080 $('#RESFeatureRequest').fadeIn();
2081 $.getJSON('http://redditenhancementsuite.com/knownfeaturerequests.json', function(data) {
2082 $('#RESKnownFeatureRequests').html('');
2083 $.each(data, function(key, val) {
2084 $('#RESKnownFeatureRequests').append('<li><a target="_blank" href="'+val.url+'">'+val.description+'</a></li>');
2091 $('#submittingBug').click(
2093 $('#sr-autocomplete').val('RESIssues');
2094 $('li a.text-button').click();
2095 $('#submittingToEnhancement').fadeOut();
2097 var txt = "- RES Version: " + RESVersion + "\n";
2098 txt += "- Browser: " + BrowserDetect.browser + "\n";
2099 if (typeof navigator === 'undefined') navigator = window.navigator;
2100 txt+= "- Browser Version: " + BrowserDetect.version + "\n";
2101 txt+= "- Cookies Enabled: " + navigator.cookieEnabled + "\n";
2102 txt+= "- Platform: " + BrowserDetect.OS + "\n";
2103 txt+= "- Did you search /r/RESIssues before submitting this: No. That, or I didn't notice this text here and edit it!\n\n";
2104 $('.usertext-edit textarea').val(txt);
2105 title.value = '[bug] Please describe your bug here. If you have screenshots, please link them in the selftext.';
2108 $('#submittingFeature').click(
2110 $('#sr-autocomplete').val('Enhancement');
2111 $('#submittingToEnhancement').fadeOut();
2112 title.value = '[feature request] Please summarize your feature request here, and elaborate in the selftext.';
2115 $('#RESSubmitOther').click(
2117 $('#sr-autocomplete').val('Enhancement');
2118 $('#submittingToEnhancement').fadeOut();
2122 $('#submittingToEnhancement').fadeIn();
2124 } else if (typeof this.submittingToEnhancement !== 'undefined') {
2125 this.submittingToEnhancement.parentNode.removeChild(this.submittingToEnhancement);
2126 if (title.value === 'Submitting a bug? Please read the box above...') {
2133 isEmpty: function(obj) {
2134 for(var prop in obj) {
2135 if(obj.hasOwnProperty(prop))
2140 deleteCookie: function(cookieName) {
2142 requestType: 'deleteCookie',
2146 if (BrowserDetect.isChrome()) {
2147 chrome.extension.sendMessage(requestJSON);
2148 } else if (BrowserDetect.isSafari()) {
2149 document.cookie = cookieName + '=null;expires=' + new Date() +'; path=/;domain=reddit.com';
2150 } else if (BrowserDetect.isOpera()) {
2151 document.cookie = cookieName + '=null;expires=' + new Date() +'; path=/;domain=reddit.com';
2152 } else if (BrowserDetect.isFirefox()) {
2153 self.postMessage(requestJSON);
2156 openLinkInNewTab: function(url, focus) {
2158 if (BrowserDetect.isChrome()) {
2160 requestType: 'openLinkInNewTab',
2164 // send message to background.html to open new tabs...
2165 chrome.extension.sendMessage(thisJSON);
2166 } else if (BrowserDetect.isSafari()) {
2168 requestType: 'openLinkInNewTab',
2172 safari.self.tab.dispatchMessage("openLinkInNewTab", thisJSON);
2173 } else if (BrowserDetect.isOpera()) {
2175 requestType: 'openLinkInNewTab',
2179 opera.extension.postMessage(JSON.stringify(thisJSON));
2180 } else if (BrowserDetect.isFirefox()) {
2182 requestType: 'openLinkInNewTab',
2186 self.postMessage(thisJSON);
2191 notification: function(contentObj, delay) {
2193 if (typeof contentObj.message === 'undefined') {
2194 if (typeof contentObj === 'string') {
2195 content = contentObj;
2200 content = contentObj.message;
2204 if (contentObj.header) {
2205 header = contentObj.header;
2209 if (contentObj.moduleID && modules[contentObj.moduleID]) {
2210 header.push(modules[contentObj.moduleID].moduleName);
2213 if (contentObj.type === 'error') {
2214 header.push('Error');
2216 header.push('Notification');
2220 header = header.join(' ');
2223 if (contentObj.moduleID && modules[contentObj.moduleID]) {
2224 header += modules['settingsNavigation'].makeUrlHashLink(contentObj.moduleID, contentObj.optionKey, ' ', 'gearIcon');
2228 if (typeof this.notificationCount === 'undefined') {
2229 this.adFrame = document.body.querySelector('#ad-frame');
2231 this.adFrame.style.display = 'none';
2233 this.notificationCount = 0;
2234 this.notificationTimers = [];
2235 this.RESNotifications = createElementWithID('div','RESNotifications');
2236 document.body.appendChild(this.RESNotifications);
2238 var thisNotification = document.createElement('div');
2239 thisNotification.classList.add('RESNotification');
2240 thisNotification.setAttribute('id','RESNotification-'+this.notificationCount);
2241 $(thisNotification).html('<div class="RESNotificationHeader"><h3>'+header+'</h3><div class="RESNotificationClose RESCloseButton">×</div></div><div class="RESNotificationContent">'+content+'</div>');
2242 var thisNotificationCloseButton = thisNotification.querySelector('.RESNotificationClose');
2243 thisNotificationCloseButton.addEventListener('click', function(e) {
2244 var thisNotification = e.target.parentNode.parentNode;
2245 RESUtils.closeNotification(thisNotification);
2247 this.setCloseNotificationTimer(thisNotification, delay);
2248 this.RESNotifications.style.display = 'block';
2249 this.RESNotifications.appendChild(thisNotification);
2250 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'notification');
2251 RESUtils.fadeElementIn(thisNotification, 0.2, 1);
2252 this.notificationCount++;
2254 setCloseNotificationTimer: function(e, delay) {
2255 delay = delay || 3000;
2256 var thisNotification = (typeof e.currentTarget !== 'undefined') ? e.currentTarget : e;
2257 var thisNotificationID = thisNotification.getAttribute('id').split('-')[1];
2258 thisNotification.classList.add('timerOn');
2259 clearTimeout(RESUtils.notificationTimers[thisNotificationID]);
2260 var thisTimer = setTimeout(function() {
2261 RESUtils.closeNotification(thisNotification);
2263 RESUtils.notificationTimers[thisNotificationID] = thisTimer;
2264 thisNotification.addEventListener('mouseover',RESUtils.cancelCloseNotificationTimer, false);
2265 thisNotification.removeEventListener('mouseout',RESUtils.setCloseNotification,false);
2267 cancelCloseNotificationTimer: function(e) {
2268 var thisNotificationID = e.currentTarget.getAttribute('id').split('-')[1];
2269 e.currentTarget.classList.remove('timerOn');
2270 clearTimeout(RESUtils.notificationTimers[thisNotificationID]);
2271 e.target.removeEventListener('mouseover',RESUtils.cancelCloseNotification,false);
2272 e.currentTarget.addEventListener('mouseout',RESUtils.setCloseNotificationTimer, false);
2274 closeNotification: function(ele) {
2275 RESUtils.fadeElementOut(ele, 0.1, RESUtils.notificationClosed);
2277 notificationClosed: function(ele) {
2278 var notifications = RESUtils.RESNotifications.querySelectorAll('.RESNotification');
2280 for (var i=0, len=notifications.length; i<len; i++) {
2281 if (notifications[i].style.opacity === '0') {
2282 notifications[i].parentNode.removeChild(notifications[i]);
2286 if (destroyed == notifications.length) {
2287 RESUtils.RESNotifications.style.display = 'none';
2288 if (RESUtils.adFrame) RESUtils.adFrame.style.display = 'block';
2291 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'notification');
2293 toggleButton: function(fieldID, enabled, onText, offText, isTable) {
2294 enabled = enabled || false;
2295 var checked = (enabled) ? 'CHECKED' : '';
2296 onText = onText || 'on';
2297 offText = offText || 'off';
2298 var thisToggle = document.createElement('div');
2299 thisToggle.setAttribute('class','toggleButton');
2300 thisToggle.setAttribute('id',fieldID+'Container');
2303 tableAttr = ' tableOption="true"';
2305 $(thisToggle).html('<span class="toggleOn">'+onText+'</span><span class="toggleOff">'+offText+'</span><input id="'+fieldID+'" type="checkbox" '+tableAttr+checked+'>');
2306 thisToggle.addEventListener('click', function(e) {
2307 var thisCheckbox = this.querySelector('input[type=checkbox]');
2308 var enabled = thisCheckbox.checked;
2309 thisCheckbox.checked = !enabled;
2311 this.classList.remove('enabled');
2313 this.classList.add('enabled');
2316 if (enabled) thisToggle.classList.add('enabled');
2319 addCommas: function(nStr) {
2321 var x = nStr.split('.');
2323 var x2 = x.length > 1 ? '.' + x[1] : '';
2324 var rgx = /(\d+)(\d{3})/;
2325 while (rgx.test(x1)) {
2326 x1 = x1.replace(rgx, '$1' + ',' + '$2');
2330 generateTable: function(items, call, context) {
2331 if (!items || !call) return;
2332 // Sanitize single item into items array
2333 if (!(items.length && typeof items !== "string")) items = [ items ];
2335 var description = [];
2336 description.push('<table>');
2338 for (var i = 0; i < items.length; i++) {
2339 var item = call(items[i], i, items, context);
2340 if (typeof item === "string") {
2341 description.push(item);
2342 } else if (item.length) {
2343 description = description.concat(item);
2346 description.push('</table>');
2347 description = description.join('\n');
2351 xhrCache: function(operation) {
2353 requestType: 'XHRCache',
2354 operation: operation
2356 if (BrowserDetect.isChrome()) {
2357 chrome.extension.sendMessage(thisJSON);
2358 } else if (BrowserDetect.isSafari()) {
2359 safari.self.tab.dispatchMessage('XHRCache', thisJSON);
2360 } else if (BrowserDetect.isOpera()) {
2361 opera.extension.postMessage(JSON.stringify(thisJSON));
2362 } else if (BrowserDetect.isFirefox()) {
2363 self.postMessage(thisJSON);
2366 initObservers: function() {
2367 var siteTable, observer;
2368 if (RESUtils.pageType() !== 'comments') {
2369 // initialize sitetable observer...
2370 siteTable = document.querySelector('#siteTable');
2371 var stMultiCheck = document.querySelectorAll('#siteTable');
2372 if (stMultiCheck.length === 2) {
2373 siteTable = stMultiCheck[1];
2376 if (BrowserDetect.MutationObserver && siteTable) {
2377 observer = new BrowserDetect.MutationObserver(function(mutations) {
2378 mutations.forEach(function(mutation) {
2379 if (mutation.addedNodes[0].id.indexOf('siteTable') !== -1) {
2380 // when a new sitetable is loaded, we need to add new observers for selftexts within that sitetable...
2381 $(mutation.addedNodes[0]).find('.entry div.expando').each(function() {
2382 RESUtils.addSelfTextObserver(this);
2384 RESUtils.watchers.siteTable.forEach(function(callback) {
2385 if (callback) callback(mutation.addedNodes[0]);
2391 observer.observe(siteTable, {
2394 characterData: false
2397 // Opera doesn't support MutationObserver - so we need this for Opera support.
2399 siteTable.addEventListener('DOMNodeInserted', function(event) {
2400 if ((event.target.tagName === 'DIV') && (event.target.getAttribute('id') && event.target.getAttribute('id').indexOf('siteTable') !== -1)) {
2401 RESUtils.watchers.siteTable.forEach(function(callback) {
2402 if (callback) callback(event.target);
2409 // initialize sitetable observer...
2410 siteTable = document.querySelector('.commentarea > .sitetable');
2412 if (BrowserDetect.MutationObserver && siteTable) {
2413 observer = new BrowserDetect.MutationObserver(function(mutations) {
2414 mutations.forEach(function(mutation) {
2415 if (mutation.addedNodes.length > 0 && mutation.addedNodes[0].classList.contains('thing')) {
2416 var thing = mutation.addedNodes[0];
2417 var newCommentEntry = thing.querySelector('.entry');
2418 if (!$(newCommentEntry).data('alreadyDetected')) {
2419 $(newCommentEntry).data('alreadyDetected', true);
2420 $(thing).find('.child').each(function() {
2421 RESUtils.addNewCommentFormObserver(this);
2423 RESUtils.watchers.newComments.forEach(function(callback) {
2424 if (callback) callback(newCommentEntry);
2431 observer.observe(siteTable, {
2434 characterData: false
2437 // Opera doesn't support MutationObserver - so we need this for Opera support.
2439 siteTable.addEventListener('DOMNodeInserted', RESUtils.mutationEventCommentHandler, false);
2444 $('.entry div.expando').each(function() {
2445 RESUtils.addSelfTextObserver(this);
2448 // initialize new comments observers on demand, by first wiring up click listeners to "load more comments" buttons.
2449 // on click, we'll add a mutation observer...
2450 $('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
2452 // initialize new comments forms observers on demand, by first wiring up click listeners to reply buttons.
2453 // on click, we'll add a mutation observer...
2454 // $('body').delegate('ul.flat-list li a[onclick*=reply]', 'click', RESUtils.addNewCommentFormObserver);
2455 $('.thing .child').each(function() {
2456 RESUtils.addNewCommentFormObserver(this);
2460 // Opera doesn't support MutationObserver - so we need this for Opera support.
2461 mutationEventCommentHandler: function (event) {
2462 if ((event.target.tagName === 'DIV') && (event.target.classList.contains('thing'))) {
2463 // we've found a matching element - stop propagation.
2464 event.stopPropagation();
2465 // because nested DOMNodeInserted events are an absolute CLUSTER to manage,
2466 // only send individual comments through to the callback.
2467 // Otherwise, we end up calling functions on a parent, then its child (which
2468 // already got scanned when we passed in the parent), etc.
2469 var thisComment = event.target.querySelector('.entry');
2470 if (! $(thisComment).data('alreadyDetected')) {
2471 $(thisComment).data('alreadyDetected', true);
2472 // wire up listeners for new "more comments" links...
2473 $(event.target).find('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
2474 RESUtils.watchers.newComments.forEach(function(callback) {
2475 RESUtils.addNewCommentFormObserver(event.target);
2476 if (callback) callback(thisComment);
2481 addNewCommentObserverToTarget: function (e) {
2482 var ele = $(e.currentTarget).closest('.sitetable')[0];
2483 // mark this as having an observer so we don't add multiples...
2484 if (! $(ele).hasClass('hasObserver')) {
2485 $(ele).addClass('hasObserver');
2486 RESUtils.addNewCommentObserver(ele);
2489 addNewCommentObserver: function(ele) {
2490 var mutationNodeToObserve = ele;
2491 if (BrowserDetect.MutationObserver) {
2492 var observer = new BrowserDetect.MutationObserver(function(mutations) {
2493 // we need to get ONLY the nodes that are new...
2494 // get the nodeList from each mutation, find comments within it,
2495 // then call our callback on it.
2496 for (var i=0, len=mutations.length; i<len; i++) {
2497 var thisMutation = mutations[i];
2498 var nodeList = thisMutation.addedNodes;
2499 // look at the added nodes, and find comment containers.
2500 for (var j=0, jLen=nodeList.length; j<jLen; j++) {
2501 if (nodeList[j].classList.contains('thing')) {
2502 $(nodeList[j]).find('.child').each(function() {
2503 RESUtils.addNewCommentFormObserver(this);
2506 // check for "load new comments" links within this group as well...
2507 $(nodeList[j]).find('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
2509 var subComments = nodeList[j].querySelectorAll('.entry');
2510 // look at the comment containers and find actual comments...
2511 for (var k=0, kLen=subComments.length; k<kLen; k++) {
2512 var thisComment = subComments[k];
2513 if (! $(thisComment).data('alreadyDetected')) {
2514 $(thisComment).data('alreadyDetected', true);
2515 RESUtils.watchers.newComments.forEach(function(callback) {
2516 if (callback) callback(thisComment);
2524 // RESUtils.watchers.newComments.forEach(function(callback) {
2525 // // add form observers to these new comments we've found...
2526 // $(mutations[0].target).find('.thing .child').each(function() {
2527 // RESUtils.addNewCommentFormObserver(this);
2529 // // check for "load new comments" links within this group as well...
2530 // $(mutations[0].target).find('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
2531 // callback(mutations[0].target);
2534 // disconnect this observer once all callbacks have been run.
2535 // unless we have the nestedlisting class, in which case don't disconnect because that's a
2536 // bottom level load more comments where even more can be loaded after, so they all drop into this
2537 // same .sitetable div.
2538 if (! $(ele).hasClass('nestedlisting')) {
2539 observer.disconnect();
2543 observer.observe(mutationNodeToObserve, {
2546 characterData: false
2549 mutationNodeToObserve.addEventListener('DOMNodeInserted', RESUtils.mutationEventCommentHandler, false);
2552 addNewCommentFormObserver: function(ele) {
2553 var commentsFormParent = ele;
2554 if (BrowserDetect.MutationObserver) {
2555 // var mutationNodeToObserve = moreCommentsParent.parentNode.parentNode.parentNode.parentNode;
2556 var observer = new BrowserDetect.MutationObserver(function(mutations) {
2557 var form = $(mutations[0].target).children('form');
2558 if ((form) && (form.length === 1)) {
2559 RESUtils.watchers.newCommentsForms.forEach(function(callback) {
2563 var newOwnComment = $(mutations[0].target).find(' > div.sitetable > .thing:first-child'); // assumes new comment will be prepended to sitetable's children
2564 if ((newOwnComment) && (newOwnComment.length === 1)) {
2565 // new comment detected from the current user...
2566 RESUtils.watchers.newComments.forEach(function(callback) {
2567 callback(newOwnComment[0]);
2573 observer.observe(commentsFormParent, {
2576 characterData: false
2579 // Opera doesn't support MutationObserver - so we need this for Opera support.
2580 commentsFormParent.addEventListener('DOMNodeInserted', function(event) {
2581 // TODO: proper tag filtering here, it's currently all wrong.
2582 if (event.target.tagName === 'FORM') {
2583 RESUtils.watchers.newCommentsForms.forEach(function(callback) {
2584 if (callback) callback(event.target);
2587 var newOwnComment = $(event.target).find(' > div.sitetable > .thing:first-child'); // assumes new comment will be prepended to sitetable's children
2588 if ((newOwnComment) && (newOwnComment.length === 1)) {
2589 // new comment detected from the current user...
2590 RESUtils.watchers.newComments.forEach(function(callback) {
2591 callback(newOwnComment[0]);
2598 addSelfTextObserver: function(ele) {
2599 var selfTextParent = ele;
2600 if (BrowserDetect.MutationObserver) {
2601 // var mutationNodeToObserve = moreCommentsParent.parentNode.parentNode.parentNode.parentNode;
2602 var observer = new BrowserDetect.MutationObserver(function(mutations) {
2603 var form = $(mutations[0].target).find('form');
2604 if ((form) && (form.length > 0)) {
2605 RESUtils.watchers.selfText.forEach(function(callback) {
2611 observer.observe(selfTextParent, {
2614 characterData: false
2617 // Opera doesn't support MutationObserver - so we need this for Opera support.
2618 selfTextParent.addEventListener('DOMNodeInserted', function(event) {
2619 // TODO: proper tag filtering here, it's currently all wrong.
2620 if (event.target.tagName === 'FORM') {
2621 RESUtils.watchers.selfText.forEach(function(callback) {
2622 if (callback) callback(event.target);
2628 watchForElement: function(type, callback) {
2631 RESUtils.watchers.siteTable.push(callback);
2634 RESUtils.watchers.newComments.push(callback);
2637 RESUtils.watchers.selfText.push(callback);
2639 case 'newCommentsForms':
2640 RESUtils.watchers.newCommentsForms.push(callback);
2648 newCommentsForms: []
2650 // A link is a comment code if all these conditions are true:
2651 // * It has no content (i.e. content.length === 0)
2652 // * Its href is of the form "/code"
2654 // In case it's not clear, here is a list of some common comment
2655 // codes on a specific subreddit:
2656 // http://www.reddit.com/r/metarage/comments/p3eqe/full_updated_list_of_comment_faces_wcodes/
2657 COMMENT_CODE_REGEX: /^\/\w+$/,
2658 isCommentCode: function (link) {
2659 var content = link.innerHTML;
2661 // Note that link.href will return the full href (which includes the
2662 // reddit.com domain). We don't want that.
2663 var href = link.getAttribute("href");
2665 return !content && this.COMMENT_CODE_REGEX.test(href);
2668 Starts a unique named timeout.
2669 If there is a running timeout with the same name cancel the old one in favor of the new.
2670 Call with no time/call parameter (null/undefined/missing) to and existing one with the given name.
2671 Used to derfer an action until a series of events has stopped.
2672 e.g. wait until a user a stopped typing to update a comment preview.
2673 (name based on similar function in underscore.js)
2675 debounceTimeouts: {},
2676 debounce: function(name, time, call, data) {
2677 if (name == null) return;
2678 if (RESUtils.debounceTimeouts[name] !== undefined) {
2679 window.clearTimeout(RESUtils.debounceTimeouts[name]);
2680 delete RESUtils.debounceTimeouts[name];
2682 if (time !== null && call !== null) {
2683 RESUtils.debounceTimeouts[name] = window.setTimeout(function() {
2684 delete RESUtils.debounceTimeouts[name];
2691 Iterate through an array in chunks, executing a callback on each element.
2692 Each chunk is handled asynchronously from the others with a delay betwen each batch.
2693 If the provided callback returns false iteration will be halted.
2695 forEachChunked: function(array, chunkSize, delay, call) {
2696 if (typeof array === 'undefined' || array === null) return;
2697 if (typeof chunkSize === 'undefined' || chunkSize === null || chunkSize < 1) return;
2698 if (typeof delay === 'undefined' || delay === null || delay < 0) return;
2699 if (typeof call === 'undefined' || call === null) return;
2701 var length = array.length;
2702 function doChunk() {
2703 for (var end = Math.min(array.length, counter+chunkSize); counter < end; counter++) {
2704 var ret = call(array[counter], counter, array);
2705 if (ret === false) return;
2707 if (counter < array.length) {
2708 window.setTimeout(doChunk, delay);
2711 window.setTimeout(doChunk, delay);
2713 getComputedStyle: function(elem, property){
2714 if (elem.constructor === String) {
2715 elem = document.querySelector(elem);
2716 } else if (!(elem instanceof Node)) {
2720 if(document.defaultView && document.defaultView.getComputedStyle) {
2721 strValue = document.defaultView.getComputedStyle(elem, "").getPropertyValue(property);
2722 } else if(elem.currentStyle){
2723 property = property.replace(/\-(\w)/g, function(strMatch, p1){
2724 return p1.toUpperCase();
2726 strValue = oElm.currentStyle[property];
2736 closeOnMouseOut: true
2740 The contents of state are as follows:
2742 //The DOM element that triggered the hover popup.
2744 //Resolved values for timing, etc.
2746 //Usecase specific object
2753 begin: function(onElement, conf, callback, context) {
2754 var hover = RESUtils.hover;
2755 if (hover.container === null) hover.create();
2756 if (hover.state !== null) {
2759 var state = hover.state = {
2761 options: $.extend({}, hover.defaults, conf),
2765 hover.showTimer = setTimeout(function() {
2766 hover.cancelShowTimer();
2767 hover.clearShowListeners();
2770 hover.state.element.addEventListener('mouseout', hover.startHideTimer, false);
2771 }, state.options.openDelay);
2773 state.element.addEventListener('click', hover.cancelShow, false);
2774 state.element.addEventListener('mouseout', hover.cancelShow, false);
2777 create: function() {
2778 var container = $('<div id="RESHoverContainer" class="RESDialogSmall"> \
2779 <h3 id="RESHoverTitle"></h3> \
2780 <div class="RESCloseButton">x</div> \
2781 <div id="RESHoverBody" class="RESDialogContents"> \
2784 document.body.appendChild(container);
2786 $(container).hover(function() {
2787 if (RESUtils.hover.state !== null) {
2788 RESUtils.hover.cancelHideTimer();
2791 if (RESUtils.hover.state !== null) {
2792 RESUtils.hover.cancelHideTimer();
2793 if (RESUtils.hover.state.options.closeOnMouseOut === true) {
2794 RESUtils.hover.startHideTimer();
2799 $(container).on('click', '.RESCloseButton', function() {
2800 RESUtils.hover.close(true);
2802 RESUtils.hover.container = container;
2806 css += '#RESHoverContainer { display: none; position: absolute; z-index: 10001; }';
2807 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; }';
2808 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; }';
2809 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; }';
2810 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; }';
2812 RESUtils.addCSS(css);
2815 var hover = RESUtils.hover;
2816 var def = $.Deferred();
2818 .progress(hover.set)
2821 hover.state.callback(def, hover.state.element, hover.state.context);
2823 set: function(header, body) {
2824 var hover = RESUtils.hover;
2825 var container = hover.container;
2826 if (header != null) $('#RESHoverTitle').empty().append(header);
2827 if (body != null) $('#RESHoverBody').empty().append(body);
2829 var XY=RESUtils.getXYpos(hover.state.element);
2831 var width = $(hover.state.element).width();
2832 var tooltipWidth = $(container).width();
2833 tooltipWidth = hover.state.options.width;
2835 RESUtils.fadeElementIn(hover.container, hover.state.options.fadeSpeed);
2836 if((window.innerWidth-XY.x-width)<=tooltipWidth){
2837 // tooltip would go off right edge - reverse it.
2838 container.classList.add('right');
2841 left: XY.x - tooltipWidth - 30,
2845 container.classList.remove('right');
2848 left: XY.x + width + 25,
2853 cancelShow: function(e) {
2854 RESUtils.hover.close(true);
2856 clearShowListeners: function() {
2857 if (RESUtils.hover.state === null) return;
2858 var element = RESUtils.hover.state.element;
2859 var func = RESUtils.hover.cancelShow;
2861 element.removeEventListener('click', func, false);
2862 element.removeEventListener('mouseout', func, false);
2864 cancelShowTimer: function() {
2865 if (RESUtils.hover.showTimer === null) return;
2866 clearTimeout(RESUtils.hover.showTimer);
2867 RESUtils.hover.showTimer = null;
2869 startHideTimer: function() {
2870 if (RESUtils.hover.state !== null) {
2871 RESUtils.hover.hideTimer = setTimeout(function() {
2872 RESUtils.hover.cancelHideTimer();
2873 RESUtils.hover.close(true);
2874 }, RESUtils.hover.state.options.fadeDelay);
2877 cancelHideTimer: function() {
2878 var hover = RESUtils.hover;
2879 if (RESUtils.hover.state !== null) {
2880 hover.state.element.removeEventListener('mouseout', hover.startHideTimer, false);
2882 if (hover.hideTimer === null) return;
2883 clearTimeout(hover.hideTimer);
2884 hover.hideTimer = null;
2886 close: function(fade) {
2887 var hover = RESUtils.hover;
2888 function afterHide() {
2889 $('#RESHoverTitle, #RESHoverBody').empty();
2890 hover.clearShowListeners();
2891 hover.cancelShowTimer();
2892 hover.cancelHideTimer();
2895 if (fade && hover.state !== null) {
2896 RESUtils.fadeElementOut(hover.container, hover.state.options.fadeSpeed, afterHide);
2898 $(hover.container).hide(afterHide);
2905 // Create a nice alert function...
2910 init: function(callback) {
2912 var alertCSS = '#alert_message { ' +
2915 'background-color: #EFEFEF;' +
2916 'border: 1px solid black;' +
2918 'font-size: 10px;' +
2920 'padding-left: 60px;' +
2921 'padding-right: 60px;' +
2922 'position: fixed!important;' +
2923 'position: absolute;' +
2926 'z-index: 1000000201;' +
2927 'text-align: left;' +
2931 '#alert_message .button {' +
2932 'border: 1px solid black;' +
2933 'font-weight: bold;' +
2934 'font-size: 10px;' +
2936 'padding-left: 7px;' +
2937 'padding-right: 7px;' +
2939 'background-color: #DFDFDF;' +
2940 'cursor: pointer;' +
2942 '#alert_message span {' +
2944 'margin-bottom: 15px; ' +
2946 '#alert_message_background {' +
2947 'position: fixed; top: 0; left: 0; bottom: 0; right: 0;' +
2948 'background-color: #333333; z-index: 100000200;' +
2951 GM_addStyle(alertCSS);
2953 gdAlert.populateContainer(callback);
2957 populateContainer: function(callback) {
2958 gdAlert.container = createElementWithID('div','alert_message');
2959 gdAlert.container.appendChild(document.createElement('span'));
2960 if (typeof callback === 'function') {
2961 this.okButton = document.createElement('input');
2962 this.okButton.setAttribute('type','button');
2963 this.okButton.setAttribute('value','confirm');
2964 this.okButton.addEventListener('click',callback, false);
2965 this.okButton.addEventListener('click',gdAlert.close, false);
2966 var closeButton = document.createElement('input');
2967 closeButton.setAttribute('type','button');
2968 closeButton.setAttribute('value','cancel');
2969 closeButton.addEventListener('click',gdAlert.close, false);
2970 gdAlert.container.appendChild(this.okButton);
2971 gdAlert.container.appendChild(closeButton);
2973 /* if (this.okButton) {
2974 gdAlert.container.removeChild(this.okButton);
2975 delete this.okButton;
2977 var closeButton = document.createElement('input');
2978 closeButton.setAttribute('type','button');
2979 closeButton.setAttribute('value','ok');
2980 closeButton.addEventListener('click',gdAlert.close, false);
2981 gdAlert.container.appendChild(closeButton);
2983 var br = document.createElement('br');
2984 br.setAttribute('style','clear: both');
2985 gdAlert.container.appendChild(br);
2986 document.body.appendChild(gdAlert.container);
2988 open: function(text, callback) {
2989 if (gdAlert.isOpen) {
2992 gdAlert.isOpen = true;
2993 gdAlert.populateContainer(callback);
2996 // gdAlert.container.getElementsByTagName("SPAN")[0].innerHTML = text;
2997 $(gdAlert.container.getElementsByTagName("SPAN")[0]).html(text);
2998 gdAlert.container.getElementsByTagName("INPUT")[0].focus();
2999 gdAlert.container.getElementsByTagName("INPUT")[0].focus();
3001 //create site overlay
3002 gdAlert.overlay = createElementWithID("div", "alert_message_background");
3003 document.body.appendChild(gdAlert.overlay);
3005 // center messagebox (requires prototype functions we don't have, so we'll redefine...)
3006 // var arrayPageScroll = document.viewport.getScrollOffsets();
3007 // var winH = arrayPageScroll[1] + (document.viewport.getHeight());
3008 // var lightboxLeft = arrayPageScroll[0];
3009 var arrayPageScroll = [ document.documentElement.scrollLeft , document.documentElement.scrollTop ];
3010 var winH = arrayPageScroll[1] + (window.innerHeight);
3011 var lightboxLeft = arrayPageScroll[0];
3013 gdAlert.container.style.top = ((winH / 2) - 90) + "px";
3014 gdAlert.container.style.left = ((gdAlert.getPageSize()[0] / 2) - 155) + "px";
3017 new Effect.Appear(gdAlert.container, {duration: 0.2});
3018 new Effect.Opacity(gdAlert.overlay, {duration: 0.2, to: 0.8});
3020 RESUtils.fadeElementIn(gdAlert.container, 0.3);
3021 RESUtils.fadeElementIn(gdAlert.overlay, 0.3);
3022 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'gdAlert');
3026 gdAlert.isOpen = false;
3028 new Effect.Fade(gdAlert.container, {duration: 0.3});
3029 new Effect.Fade(gdAlert.overlay, {duration: 0.3, afterFinish: function() {
3030 document.body.removeChild(gdAlert.overlay);
3033 RESUtils.fadeElementOut(gdAlert.container, 0.3);
3034 RESUtils.fadeElementOut(gdAlert.overlay, 0.3);
3035 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'gdAlert');
3038 getPageSize: function() {
3039 var xScroll, yScroll;
3040 if (window.innerHeight && window.scrollMaxY) {
3041 xScroll = window.innerWidth + window.scrollMaxX;
3042 yScroll = window.innerHeight + window.scrollMaxY;
3043 } else if (document.body.scrollHeight > document.body.offsetHeight){ // all but Explorer Mac
3044 xScroll = document.body.scrollWidth;
3045 yScroll = document.body.scrollHeight;
3046 } else { // Explorer Mac...would also work in Explorer 6 Strict, Mozilla and Safari
3047 xScroll = document.body.offsetWidth;
3048 yScroll = document.body.offsetHeight;
3051 var windowWidth, windowHeight;
3053 if (self.innerHeight) { // all except Explorer
3054 if(document.documentElement.clientWidth){
3055 windowWidth = document.documentElement.clientWidth;
3057 windowWidth = self.innerWidth;
3059 windowHeight = self.innerHeight;
3060 } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
3061 windowWidth = document.documentElement.clientWidth;
3062 windowHeight = document.documentElement.clientHeight;
3063 } else if (document.body) { // other Explorers
3064 windowWidth = document.body.clientWidth;
3065 windowHeight = document.body.clientHeight;
3068 // for small pages with total height less then height of the viewport
3069 if(yScroll < windowHeight){
3070 pageHeight = windowHeight;
3072 pageHeight = yScroll;
3075 // for small pages with total width less then width of the viewport
3076 if(xScroll < windowWidth){
3077 pageWidth = xScroll;
3079 pageWidth = windowWidth;
3081 return [pageWidth,pageHeight];
3085 //overwrite the alert function
3086 var alert = function(text, callback) {
3087 if (gdAlert.container === false) {
3088 gdAlert.init(callback);
3090 gdAlert.open(text, callback);
3093 // this function copies localStorage (from the GM import script) to FF addon simplestorage...
3094 function GMSVtoFFSS() {
3095 var console = unsafeWindow.console;
3096 for (var key in localStorage) {
3097 RESStorage.setItem(key, localStorage[key]);
3099 localStorage.setItem('copyComplete','true');
3100 localStorage.removeItem('RES.lsTest');
3101 RESUtils.notification('Data transfer complete. You may now uninstall the Greasemonkey script');
3104 // jquery plugin CSS
3105 RESUtils.addCSS(tokenizeCSS);
3106 RESUtils.addCSS(guidersCSS);
3108 // define the RESConsole class
3111 RESConsoleContainer: '',
3113 RESConfigPanelOptions: null,
3114 // make the modules panel accessible to this class for updating (i.e. when preferences change, so we can redraw it)
3115 RESConsoleConfigPanel: createElementWithID('div', 'RESConsoleConfigPanel', 'RESPanel'),
3116 RESConsoleAboutPanel: createElementWithID('div', 'RESConsoleAboutPanel', 'RESPanel'),
3117 RESConsoleProPanel: createElementWithID('div', 'RESConsoleProPanel', 'RESPanel'),
3118 addConsoleLink: function() {
3119 this.userMenu = document.querySelector('#header-bottom-right');
3120 if (this.userMenu) {
3121 var RESPrefsLink = $("<span id='openRESPrefs'><span id='RESSettingsButton' title='RES Settings' class='gearIcon'></span>")
3122 .mouseenter(RESConsole.showPrefsDropdown);
3123 $(this.userMenu).find("ul").after(RESPrefsLink).after("<span class='separator'>|</span>");
3124 this.RESPrefsLink = RESPrefsLink[0];
3127 addConsoleDropdown: function() {
3128 this.gearOverlay = createElementWithID('div','RESMainGearOverlay');
3129 this.gearOverlay.setAttribute('class','RESGearOverlay');
3130 $(this.gearOverlay).html('<div class="gearIcon"></div>');
3132 this.prefsDropdown = createElementWithID('div','RESPrefsDropdown','RESDropdownList');
3133 $(this.prefsDropdown).html('<ul id="RESDropdownOptions"><li id="SettingsConsole">settings console</li><li id="RES-donate">donate to RES</li></ul>');
3134 var thisSettingsButton = this.prefsDropdown.querySelector('#SettingsConsole');
3135 this.settingsButton = thisSettingsButton;
3136 thisSettingsButton.addEventListener('click', function() {
3137 RESConsole.hidePrefsDropdown();
3140 var thisDonateButton = this.prefsDropdown.querySelector('#RES-donate');
3141 thisDonateButton.addEventListener('click', function() {
3142 RESUtils.openLinkInNewTab('http://redditenhancementsuite.com/contribute.html', true);
3144 $(this.prefsDropdown).mouseleave(function() {
3145 RESConsole.hidePrefsDropdown();
3147 $(this.prefsDropdown).mouseenter(function() {
3148 clearTimeout(RESConsole.prefsTimer);
3150 $(this.gearOverlay).mouseleave(function() {
3151 RESConsole.prefsTimer = setTimeout(function() {
3152 RESConsole.hidePrefsDropdown();
3155 document.body.appendChild(this.gearOverlay);
3156 document.body.appendChild(this.prefsDropdown);
3157 if (RESStorage.getItem('RES.newAnnouncement','true')) {
3158 RESUtils.setNewNotification();
3161 showPrefsDropdown: function(e) {
3162 var thisTop = parseInt($(RESConsole.userMenu).offset().top + 1, 10);
3163 // var thisRight = parseInt($(window).width() - $(RESConsole.RESPrefsLink).offset().left);
3164 // thisRight = 175-thisRight;
3165 var thisLeft = parseInt($(RESConsole.RESPrefsLink).offset().left - 6, 10);
3166 // $('#RESMainGearOverlay').css('left',thisRight+'px');
3167 $('#RESMainGearOverlay').css('height',$('#header-bottom-right').outerHeight()+'px');
3168 $('#RESMainGearOverlay').css('left',thisLeft+'px');
3169 $('#RESMainGearOverlay').css('top',thisTop+'px');
3170 RESConsole.prefsDropdown.style.top = parseInt(thisTop+$(RESConsole.userMenu).outerHeight(), 10)+'px';
3171 RESConsole.prefsDropdown.style.right = '0px';
3172 RESConsole.prefsDropdown.style.display = 'block';
3173 $('#RESMainGearOverlay').show();
3174 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'prefsDropdown');
3176 hidePrefsDropdown: function(e) {
3177 RESConsole.RESPrefsLink.classList.remove('open');
3178 $('#RESMainGearOverlay').hide();
3179 RESConsole.prefsDropdown.style.display = 'none';
3180 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'prefsDropdown');
3182 resetModulePrefs: function() {
3185 'betteReddit': true,
3186 'singleClick': true,
3187 'subRedditTagger': true,
3188 'uppersAndDowners': true,
3189 'keyboardNav': true,
3190 'commentPreview': true,
3193 'usernameHider': false,
3194 'accountSwitcher': true,
3195 'styleTweaks': true,
3196 'filteReddit': true,
3197 'spamButton': false,
3198 'bitcointip': false,
3201 this.setModulePrefs(prefs);
3204 getAllModulePrefs: function(force) {
3206 // if we've done this before, just return the cached version
3207 if ((!force) && (typeof this.getAllModulePrefsCached !== 'undefined')) return this.getAllModulePrefsCached;
3208 // get the stored preferences out first.
3209 if (RESStorage.getItem('RES.modulePrefs') !== null) {
3210 storedPrefs = safeJSON.parse(RESStorage.getItem('RES.modulePrefs'), 'RES.modulePrefs');
3211 } else if (RESStorage.getItem('modulePrefs') !== null) {
3212 // Clean up old moduleprefs.
3213 storedPrefs = safeJSON.parse(RESStorage.getItem('modulePrefs'), 'modulePrefs');
3214 RESStorage.removeItem('modulePrefs');
3215 this.setModulePrefs(storedPrefs);
3217 // looks like this is the first time RES has been run - set prefs to defaults...
3218 storedPrefs = this.resetModulePrefs();
3220 if (storedPrefs === null) {
3223 // 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.
3225 // for any stored prefs, drop them in our prefs JSON object.
3226 for (var module in modules) {
3227 if (storedPrefs[module]) {
3228 prefs[module] = storedPrefs[module];
3229 } else if ((! modules[module].disabledByDefault) && ((storedPrefs[module] == null) || (module === 'dashboard'))) {
3230 // looks like a new module, or no preferences. We'll default it to on.
3231 // we also default dashboard to on. It's not really supposed to be disabled.
3232 prefs[module] = true;
3234 prefs[module] = false;
3237 if ((typeof prefs !== 'undefined') && (prefs !== 'undefined') && (prefs)) {
3238 this.getAllModulePrefsCached = prefs;
3242 getModulePrefs: function(moduleID) {
3244 var prefs = this.getAllModulePrefs();
3245 return prefs[moduleID];
3247 alert('no module name specified for getModulePrefs');
3250 setModulePrefs: function(prefs) {
3251 if (prefs !== null) {
3252 RESStorage.setItem('RES.modulePrefs', JSON.stringify(prefs));
3255 alert('error - no prefs specified');
3258 create: function() {
3259 // create the console container
3260 this.RESConsoleContainer = createElementWithID('div', 'RESConsole');
3261 // hide it by default...
3262 // this.RESConsoleContainer.style.display = 'none';
3263 // create a modal overlay
3264 this.modalOverlay = createElementWithID('div', 'modalOverlay');
3265 this.modalOverlay.addEventListener('click', function(e) {
3269 document.body.appendChild(this.modalOverlay);
3270 // create the header
3271 var RESConsoleHeader = createElementWithID('div', 'RESConsoleHeader');
3272 // create the top bar and place it in the header
3273 var RESConsoleTopBar = createElementWithID('div', 'RESConsoleTopBar');
3274 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=';
3275 // this string is split because a specific sequence of characters screws up some git clients into thinking this file is binary.
3276 this.loader = 'data:image/gif;base64,R0lGODlhHQAWANUAAESatESetEyetEyitEyivFSivFSmvFymvFyqvGSqvGSqxGSuxGyuxGyyxHSyxHS2xHS2zHy2zHy6zIS6zIS+zIy+zIzCzIzC1JTG1JzK1JzK3JzO3KTO3KTS3KzS3KzW3LTW3LTW5LTa5Lza5Lze5MTe5MTi5MTi7Mzi7Mzm7NTm7NTq7Nzq7Nzq9Nzu9OTu9OTy9Ozy9Oz29Oz2/PT2/PT6/Pz6/Pz+/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH';
3277 this.loader += '/C05FVFNDQVBFMi4wAwEAAAAh/h1CdWlsdCB3aXRoIEdJRiBNb3ZpZSBHZWFyIDQuMAAh+QQIBgAAACwAAAAAHQAWAAAG/sCbcEgs3myyEIzjQr2MUGjrgpFMrJIMhxTtei4SbPhKwXCeXaLren00GIuHlSLxzNJDD4NOWST8CwsUgxEjeEIcDYN0ICkjFA4UFYMcRXckIS8XKysTCJKSGCMkHBUXpwwXRC8UGheLpgsMDBKmF6YWF7kODYY3LmawoKcXCxIKFMSnkBIELDczIxODk2SmpoMFbg8XDg4SAAoTNTUY1BcTDQsKCw2nGGAMBAUJDQcCDZ8yNzESya8NFDCAEFAChoO6GGSowEDDggsq0HhIZisVixkwQFDBkIHCARQ1XICosSIGEYe5MFjAsE8IigwcYWa402VEyoNmRozgkEFDbs8MBRS0jJJCwAOcMn1u4MBTA4UHNdLIgIAOg08NGphqZWAggohDHBIEqMCRqZYMEjZMMPBgaJcYcDAcQMBhwgMOGOg9AOHrUIkQ8hJQQKDgQaQFEQ4ZuRABxSwREtqWcKHYiIwaWm6UGBG18o0gACH5BAgGAAAALAAAAAAdABYAAAb';
3278 this.loader += '+wJtwSCwKXabWBjaS2YxQowqDkUysEg4GFe1+LtgrVkKddYsvCRbSYCwcEgpl4jGfhR3GnLJILP4JchQQJXdCHhCCEiApIxUNFZESGkUzNCsaMBwjMRQFE3IVGCMkHBYXFBcQGEM1NhRUexWqCRAQsxcWuBcXEQgkQjEXGYIUFanIDxENEry5F48SByo3MCWCx1fGzlcHCxKQEggUAgYWrqjGcg0LCguQuVUNBwUJbgIKDBFmMKi4DfnYKCBDhUqDCRgWYFDmAoYQDs2cMcCwYkaMEBYKUjiAAsaMDzFgxCDiocEpDBcwjBSSIkMGDRkwWHDYJUSqghg2jBjB4eVzSwwKINA4Y0JAhIIuYcLkoKFnAwc1zsyYYCFC0pccsmZNcNCDoQ4FCmAQ1TPr2A4JClCIeufFggcUAkDg8ECCBwkF4F4YYYhlCAQFHEwwwECCAwcINDzpK2QGBQ4gFEwAsSDDDA4vGBOxUaMfFw5cNN8IAgAh+QQIBgAAACwAAAAAHQAWAAAG/sCbcEgsClcqlAc2qtWMUCOKc5FYrZyK6xmFhizWiURMxmBm3SIMMp48GoyFQ0Kpc9BpIcchpiz+';
3279 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=';
3280 RESConsoleTopBar.setAttribute('class','RESDialogTopBar');
3281 $(RESConsoleTopBar).html('<img id="RESLogo" src="'+this.logo+'"><h1>reddit enhancement suite</h1>');
3282 RESConsoleHeader.appendChild(RESConsoleTopBar);
3283 this.RESConsoleVersion = createElementWithID('div','RESConsoleVersion');
3284 $(this.RESConsoleVersion).text('v' + RESVersion);
3285 RESConsoleTopBar.appendChild(this.RESConsoleVersion);
3287 // Create the search bar and place it in the top bar
3288 var RESSearchContainer = modules['settingsNavigation'].renderSearchForm();
3289 RESConsoleTopBar.appendChild(RESSearchContainer);
3291 var RESSubredditLink = createElementWithID('a','RESConsoleSubredditLink');
3292 $(RESSubredditLink).text('/r/Enhancement');
3293 RESSubredditLink.setAttribute('href','http://reddit.com/r/Enhancement');
3294 RESSubredditLink.setAttribute('alt','The RES Subreddit');
3295 RESConsoleTopBar.appendChild(RESSubredditLink);
3296 // create the close button and place it in the header
3297 var RESClose = createElementWithID('span', 'RESClose', 'RESCloseButton');
3298 $(RESClose).text('×');
3299 RESClose.addEventListener('click', function(e) {
3303 RESConsoleTopBar.appendChild(RESClose);
3304 this.categories = [];
3305 for (var module in modules) {
3306 if ((typeof modules[module].category !== 'undefined') && (this.categories.indexOf(modules[module].category) === -1)) {
3307 this.categories.push(modules[module].category);
3310 this.categories.sort();
3312 // var menuItems = this.categories.concat(['RES Pro','About RES'));
3313 var menuItems = this.categories.concat(['About RES']);
3314 var RESMenu = createElementWithID('ul', 'RESMenu');
3315 for (var item = 0; item < menuItems.length; item++) {
3316 var thisMenuItem = document.createElement('li');
3317 $(thisMenuItem).text(menuItems[item]);
3318 thisMenuItem.setAttribute('id', 'Menu-' + menuItems[item]);
3319 thisMenuItem.addEventListener('click', function(e) {
3321 RESConsole.menuClick(this);
3323 RESMenu.appendChild(thisMenuItem);
3325 RESConsoleHeader.appendChild(RESMenu);
3326 this.RESConsoleContainer.appendChild(RESConsoleHeader);
3327 // Store the menu items in a global variable for easy access by the menu selector function.
3328 RESConsole.RESMenuItems = RESMenu.querySelectorAll('li');
3329 // Create a container for each management panel
3330 this.RESConsoleContent = createElementWithID('div', 'RESConsoleContent');
3331 this.RESConsoleContainer.appendChild(this.RESConsoleContent);
3332 // Okay, the console is done. Add it to the document body.
3333 document.body.appendChild(this.RESConsoleContainer);
3335 window.addEventListener("keydown", function(e) {
3336 if ((RESConsole.captureKey) && (e.keyCode !== 16) && (e.keyCode !== 17) && (e.keyCode !== 18)) {
3337 // capture the key, display something nice for it, and then close the popup...
3339 var keyArray = [e.keyCode, e.altKey, e.ctrlKey, e.shiftKey, e.metaKey];
3340 document.getElementById(RESConsole.captureKeyID).value = keyArray.join(",");
3341 document.getElementById(RESConsole.captureKeyID+'-display').value = RESUtils.niceKeyCode(keyArray);
3342 RESConsole.keyCodeModal.style.display = 'none';
3343 RESConsole.captureKey = false;
3347 $("#RESConsoleContent").delegate(".keycode + input[type=text][displayonly]", {
3348 focus: function(e) {
3349 var thisXY=RESUtils.getXYpos(this, true);
3350 // show dialog box to grab keycode, but display something nice...
3351 $(RESConsole.keyCodeModal).css({
3353 top: RESUtils.mouseY + "px",
3354 left: RESUtils.mouseX + "px;"
3356 // RESConsole.keyCodeModal.style.display = 'block';
3357 RESConsole.captureKey = true;
3358 RESConsole.captureKeyID = this.getAttribute('capturefor');
3361 $(RESConsole.keyCodeModal).css("display", "none");
3365 this.keyCodeModal = createElementWithID('div', 'keyCodeModal');
3366 $(this.keyCodeModal).text('Press a key (or combination with shift, alt and/or ctrl) to assign this action.');
3367 document.body.appendChild(this.keyCodeModal);
3369 drawConfigPanel: function(category) {
3370 if (!category) return;
3372 this.drawConfigPanelCategory(category);
3374 getModuleIDsByCategory: function(category) {
3375 var moduleList = [];
3376 for (var i in modules) {
3377 if (modules[i].category == category && !modules[i].hidden) moduleList.push(i);
3379 moduleList.sort(function(a,b) {
3380 if (modules[a].moduleName.toLowerCase() > modules[b].moduleName.toLowerCase()) return 1;
3386 drawConfigPanelCategory: function(category, moduleList) {
3387 $(this.RESConsoleConfigPanel).empty();
3390 var moduleTest = RESStorage.getItem('moduleTest');
3392 console.log(moduleTest);
3393 // TEST loading stored modules...
3394 var evalTest = eval(moduleTest);
3397 moduleList = moduleList || this.getModuleIDsByCategory(category);
3399 this.RESConfigPanelModulesPane = createElementWithID('div', 'RESConfigPanelModulesPane');
3400 for (var i=0, len=moduleList.length; i<len; i++) {
3401 var thisModuleButton = createElementWithID('div', 'module-'+moduleList[i]);
3402 thisModuleButton.classList.add('moduleButton');
3403 var thisModule = moduleList[i];
3404 $(thisModuleButton).text(modules[thisModule].moduleName);
3405 if (modules[thisModule].isEnabled()) {
3406 thisModuleButton.classList.add('enabled');
3408 thisModuleButton.setAttribute('moduleID', modules[thisModule].moduleID);
3409 thisModuleButton.addEventListener('click', function(e) {
3410 RESConsole.showConfigOptions(this.getAttribute('moduleID'));
3412 this.RESConfigPanelModulesPane.appendChild(thisModuleButton);
3414 this.RESConsoleConfigPanel.appendChild(this.RESConfigPanelModulesPane);
3416 this.RESConfigPanelOptions = createElementWithID('div', 'RESConfigPanelOptions');
3417 $(this.RESConfigPanelOptions).html('<h1>RES Module Configuration</h1> Select a module from the column at the left to enable or disable it, and configure its various options.');
3418 this.RESConsoleConfigPanel.appendChild(this.RESConfigPanelOptions);
3419 this.RESConsoleContent.appendChild(this.RESConsoleConfigPanel);
3421 updateSelectedModule: function (moduleID) {
3422 var moduleButtons = $(RESConsole.RESConsoleConfigPanel).find('.moduleButton');
3423 moduleButtons.removeClass('active');
3424 moduleButtons.filter(function() { return this.getAttribute('moduleID') === moduleID; })
3425 .addClass('active');
3427 drawOptionInput: function(moduleID, optionName, optionObject, isTable) {
3428 var thisOptionFormEle;
3429 switch(optionObject.type) {
3432 thisOptionFormEle = createElementWithID('textarea', optionName);
3433 thisOptionFormEle.setAttribute('type','textarea');
3434 thisOptionFormEle.setAttribute('moduleID',moduleID);
3435 $(thisOptionFormEle).html(escapeHTML(optionObject.value));
3439 thisOptionFormEle = createElementWithID('input', optionName);
3440 thisOptionFormEle.setAttribute('type','text');
3441 thisOptionFormEle.setAttribute('moduleID',moduleID);
3442 thisOptionFormEle.setAttribute('placeHolder',optionObject.placeHolder || '');
3443 thisOptionFormEle.setAttribute('value',optionObject.value);
3447 thisOptionFormEle = createElementWithID('button', optionName);
3448 thisOptionFormEle.classList.add('RESConsoleButton');
3449 thisOptionFormEle.setAttribute('moduleID',moduleID);
3450 thisOptionFormEle.innerText = optionObject.text;
3451 thisOptionFormEle.addEventListener('click', optionObject.callback, false);
3455 thisOptionFormEle = createElementWithID('input', optionName);
3456 thisOptionFormEle.setAttribute('class','RESInputList');
3457 thisOptionFormEle.setAttribute('type','text');
3458 thisOptionFormEle.setAttribute('moduleID',moduleID);
3459 // thisOptionFormEle.setAttribute('value',optionObject.value);
3460 existingOptions = optionObject.value;
3461 if (typeof existingOptions === 'undefined') existingOptions = '';
3463 var optionArray = existingOptions.split(',');
3464 for (var i=0, len=optionArray.length; i<len; i++) {
3465 if (optionArray[i] !== '') prepop.push({id: optionArray[i], name: optionArray[i]});
3467 setTimeout(function() {
3468 $(thisOptionFormEle).tokenInput(optionObject.source, {
3470 queryParam: "query",
3472 allowFreeTagging: true,
3474 onResult: (typeof optionObject.onResult === 'function') ? optionObject.onResult : null,
3475 onCachedResult: (typeof optionObject.onCachedResult === 'function') ? optionObject.onCachedResult : null,
3476 prePopulate: prepop,
3477 hintText: (typeof optionObject.hintText === 'string') ? optionObject.hintText : null
3483 thisOptionFormEle = createElementWithID('input', optionName);
3484 thisOptionFormEle.setAttribute('type','password');
3485 thisOptionFormEle.setAttribute('moduleID',moduleID);
3486 thisOptionFormEle.setAttribute('value',optionObject.value);
3491 var thisOptionFormEle = createElementWithID('input', optionName);
3492 thisOptionFormEle.setAttribute('type','checkbox');
3493 thisOptionFormEle.setAttribute('moduleID',moduleID);
3494 thisOptionFormEle.setAttribute('value',optionObject.value);
3495 if (optionObject.value) {
3496 thisOptionFormEle.setAttribute('checked',true);
3499 thisOptionFormEle = RESUtils.toggleButton(optionName, optionObject.value, null, null, isTable);
3503 if (typeof optionObject.values === 'undefined') {
3504 alert('misconfigured enum option in module: ' + moduleID);
3506 thisOptionFormEle = createElementWithID('div', optionName);
3507 thisOptionFormEle.setAttribute('class', 'enum');
3508 for (var j = 0; j < optionObject.values.length; j++) {
3509 var thisDisplay = optionObject.values[j].display;
3510 var thisValue = optionObject.values[j].value;
3511 var thisOptionFormSubEle = createElementWithID('input', optionName+'-'+j);
3512 if (isTable) thisOptionFormSubEle.setAttribute('tableOption', 'true');
3513 thisOptionFormSubEle.setAttribute('type', 'radio');
3514 thisOptionFormSubEle.setAttribute('name', optionName);
3515 thisOptionFormSubEle.setAttribute('moduleID', moduleID);
3516 thisOptionFormSubEle.setAttribute('value', optionObject.values[j].value);
3517 var nullEqualsEmpty = ((optionObject.value == null) && (optionObject.values[j].value === ''));
3518 // we also need to check for null == '' - which are technically equal.
3519 if ((optionObject.value == optionObject.values[j].value) || nullEqualsEmpty) {
3520 thisOptionFormSubEle.setAttribute('checked', 'checked');
3522 var thisOptionFormSubEleText = document.createTextNode(' ' + optionObject.values[j].name + ' ');
3523 thisOptionFormEle.appendChild(thisOptionFormSubEle);
3524 thisOptionFormEle.appendChild(thisOptionFormSubEleText);
3525 var thisBR = document.createElement('br');
3526 thisOptionFormEle.appendChild(thisBR);
3531 // keycode - shows a key value, but stores a keycode and possibly shift/alt/ctrl combo.
3532 var realOptionFormEle = $("<input>").attr({
3538 border: "1px solid red",
3540 }).val(optionObject.value);
3541 if (isTable) realOptionFormEle.attr('tableOption','true');
3543 var thisKeyCodeDisplay = $("<input>").attr({
3544 id: optionName+"-display",
3546 capturefor: optionName,
3548 }).val(RESUtils.niceKeyCode(optionObject.value));
3549 thisOptionFormEle = $("<div>").append(realOptionFormEle).append(thisKeyCodeDisplay)[0];
3552 console.log('misconfigured option in module: ' + moduleID);
3556 thisOptionFormEle.setAttribute('tableOption','true');
3558 return thisOptionFormEle;
3560 enableModule: function(moduleID, onOrOff) {
3561 var prefs = this.getAllModulePrefs(true);
3562 prefs[moduleID] = !!onOrOff;
3563 this.setModulePrefs(prefs);
3565 showConfigOptions: function(moduleID) {
3566 if (!modules[moduleID]) return;
3567 RESConsole.drawConfigOptions(moduleID);
3568 RESConsole.updateSelectedModule(moduleID);
3569 RESConsole.currentModule = moduleID;
3571 RESConsole.RESConsoleContent.scrollTop = 0;
3573 modules['settingsNavigation'].setUrlHash(moduleID);
3575 drawConfigOptions: function(moduleID) {
3576 if (modules[moduleID] && modules[moduleID].hidden) return;
3577 var thisOptions = RESUtils.getOptions(moduleID);
3580 this.RESConfigPanelOptions.setAttribute('style','display: block;');
3581 $(this.RESConfigPanelOptions).html('');
3582 // put in the description, and a button to enable/disable the module, first..
3583 var thisHeader = document.createElement('div');
3584 thisHeader.classList.add('moduleHeader');
3585 $(thisHeader).html('<span class="moduleName">' + modules[moduleID].moduleName + '</span>');
3586 var thisToggle = document.createElement('div');
3587 thisToggle.classList.add('moduleToggle');
3588 if (moduleID === 'dashboard') thisToggle.style.display = 'none';
3589 $(thisToggle).html('<span class="toggleOn">on</span><span class="toggleOff">off</span>');
3590 if (modules[moduleID].isEnabled()) thisToggle.classList.add('enabled');
3591 thisToggle.setAttribute('moduleID',moduleID);
3592 thisToggle.addEventListener('click', function(e) {
3593 var activePane = RESConsole.RESConfigPanelModulesPane.querySelector('.active');
3594 var enabled = this.classList.contains('enabled');
3596 activePane.classList.remove('enabled');
3597 this.classList.remove('enabled');
3598 RESConsole.moduleOptionsScrim.classList.add('visible');
3599 $('#moduleOptionsSave').hide();
3601 activePane.classList.add('enabled');
3602 this.classList.add('enabled');
3603 RESConsole.moduleOptionsScrim.classList.remove('visible');
3604 $('#moduleOptionsSave').fadeIn();
3606 RESConsole.enableModule(this.getAttribute('moduleID'), !enabled);
3608 thisHeader.appendChild(thisToggle);
3609 // not really looping here, just only executing if there's 1 or more options...
3610 for (var i in thisOptions) {
3611 var thisSaveButton = createElementWithID('input','moduleOptionsSave');
3612 thisSaveButton.setAttribute('type','button');
3613 thisSaveButton.setAttribute('value','save options');
3614 thisSaveButton.addEventListener('click', function(e) {
3615 RESConsole.saveCurrentModuleOptions(e);
3617 this.RESConsoleConfigPanel.appendChild(thisSaveButton);
3618 var thisSaveStatus = createElementWithID('div','moduleOptionsSaveStatus','saveStatus');
3619 thisHeader.appendChild(thisSaveStatus);
3622 var thisDescription = document.createElement('div');
3623 thisDescription.classList.add('moduleDescription');
3624 $(thisDescription).html(modules[moduleID].description);
3625 thisHeader.appendChild(thisDescription);
3626 this.RESConfigPanelOptions.appendChild(thisHeader);
3627 var allOptionsContainer = createElementWithID('div', 'allOptionsContainer');
3628 this.RESConfigPanelOptions.appendChild(allOptionsContainer);
3629 // now draw all the options...
3630 for (var i in thisOptions) {
3631 if (!(thisOptions[i].noconfig)) {
3633 var thisOptionContainer = createElementWithID('div', null, 'optionContainer');
3634 var thisLabel = document.createElement('label');
3635 thisLabel.setAttribute('for',i);
3636 $(thisLabel).text(i);
3637 var thisOptionDescription = createElementWithID('div', null, 'optionDescription');
3638 $(thisOptionDescription).html(thisOptions[i].description);
3639 thisOptionContainer.appendChild(thisLabel);
3640 if (thisOptions[i].type === 'table') {
3641 thisOptionDescription.classList.add('table');
3642 // table - has a list of fields (headers of table), users can add/remove rows...
3643 if (typeof thisOptions[i].fields === 'undefined') {
3644 alert('misconfigured table option in module: ' + moduleID + ' - options of type "table" must have fields defined');
3646 // get field names...
3647 var fieldNames = [];
3648 // now that we know the field names, get table rows...
3649 var thisTable = document.createElement('table');
3650 thisTable.setAttribute('moduleID',moduleID);
3651 thisTable.setAttribute('optionName',i);
3652 thisTable.setAttribute('class','optionsTable');
3653 var thisThead = document.createElement('thead');
3654 var thisTableHeader = document.createElement('tr'), thisTH;
3655 thisTable.appendChild(thisThead);
3656 for (var j=0;j<thisOptions[i].fields.length;j++) {
3657 fieldNames[j] = thisOptions[i].fields[j].name;
3658 thisTH = document.createElement('th');
3659 $(thisTH).text(thisOptions[i].fields[j].name);
3660 thisTableHeader.appendChild(thisTH);
3662 // add delete column
3663 thisTH = document.createElement('th');
3664 $(thisTH).text('delete');
3665 thisTableHeader.appendChild(thisTH);
3667 thisTH = document.createElement('th');
3668 $(thisTH).text('move')
3669 .attr('title', 'click, drag, and drop')
3670 .css('cursor', 'help');
3671 thisTableHeader.appendChild(thisTH);
3672 thisThead.appendChild(thisTableHeader);
3673 thisTable.appendChild(thisThead);
3674 var thisTbody = document.createElement('tbody');
3675 thisTbody.setAttribute('id','tbody_'+i);
3676 if (thisOptions[i].value) {
3677 for (var j=0;j<thisOptions[i].value.length;j++) {
3678 var thisTR = document.createElement('tr'), thisTD;
3679 $(thisTR).data('itemidx-orig', j);
3680 for (var k=0;k<thisOptions[i].fields.length;k++) {
3681 thisTD = document.createElement('td');
3682 thisTD.className = 'hasTableOption';
3683 var thisOpt = thisOptions[i].fields[k];
3684 var thisFullOpt = i + '_' + thisOptions[i].fields[k].name;
3685 thisOpt.value = thisOptions[i].value[j][k];
3686 // var thisOptInputName = thisOpt.name + '_' + j;
3687 var thisOptInputName = thisFullOpt + '_' + j;
3688 var thisTableEle = this.drawOptionInput(moduleID, thisOptInputName, thisOpt, true);
3689 thisTD.appendChild(thisTableEle);
3690 thisTR.appendChild(thisTD);
3692 // add delete button
3693 thisTD = document.createElement('td');
3694 var thisDeleteButton = document.createElement('div');
3695 thisDeleteButton.className = 'deleteButton';
3696 thisDeleteButton.addEventListener('click', RESConsole.deleteOptionRow);
3697 thisTD.appendChild(thisDeleteButton);
3698 thisTR.appendChild(thisTD);
3700 thisTD = document.createElement('td');
3701 var thisHandle = document.createElement('div');
3703 .html("⋮⋮")
3706 thisTR.appendChild(thisTD);
3707 thisTbody.appendChild(thisTR);
3710 thisTable.appendChild(thisTbody);
3711 var thisOptionFormEle = thisTable;
3713 thisOptionContainer.appendChild(thisOptionDescription);
3714 thisOptionContainer.appendChild(thisOptionFormEle);
3715 // Create an "add row" button...
3716 var addRowText = thisOptions[i].addRowText || 'Add Row';
3717 var addRowButton = document.createElement('input');
3718 addRowButton.classList.add('addRowButton');
3719 addRowButton.setAttribute('type','button');
3720 addRowButton.setAttribute('value',addRowText);
3721 addRowButton.setAttribute('optionName',i);
3722 addRowButton.setAttribute('moduleID',moduleID);
3723 addRowButton.addEventListener('click', function() {
3724 var optionName = this.getAttribute('optionName');
3725 var thisTbodyName = 'tbody_' + optionName;
3726 var thisTbody = document.getElementById(thisTbodyName);
3727 var newRow = document.createElement('tr');
3728 var rowCount = (thisTbody.querySelectorAll('tr')) ? thisTbody.querySelectorAll('tr').length + 1 : 1;
3729 for (var i=0, len=modules[moduleID].options[optionName].fields.length;i<len;i++) {
3730 var newCell = document.createElement('td');
3731 newCell.className = 'hasTableOption';
3732 var thisOpt = modules[moduleID].options[optionName].fields[i];
3733 if (thisOpt.type !== 'enum') thisOpt.value = '';
3734 var optionNameWithRow = optionName+'_'+thisOpt.name+'_'+rowCount;
3735 var thisInput = RESConsole.drawOptionInput(moduleID, optionNameWithRow, thisOpt, true);
3736 newCell.appendChild(thisInput);
3737 newRow.appendChild(newCell);
3738 $(newRow).data('option-index', rowCount - 1);
3739 var firstText = newRow.querySelector('input[type=text]');
3740 if (!firstText) firstText = newRow.querySelector('textarea');
3742 setTimeout(function() {
3747 // add delete button
3748 thisTD = document.createElement('td');
3749 var thisDeleteButton = document.createElement('div');
3750 thisDeleteButton.className = 'deleteButton';
3751 thisDeleteButton.addEventListener('click', RESConsole.deleteOptionRow);
3752 thisTD.appendChild(thisDeleteButton);
3753 newRow.appendChild(thisTD);
3755 thisTD = document.createElement('td');
3756 var thisHandle = document.createElement('div');
3758 .html("⋮⋮")
3762 var thisLen = (modules[moduleID].options[optionName].value) ? modules[moduleID].options[optionName].value.length : 0;
3763 $(thisTR).data('itemidx-orig', thisLen);
3765 thisTbody.appendChild(newRow);
3767 thisOptionContainer.appendChild(addRowButton);
3769 (function(moduleID, optionKey) {
3770 $(thisTbody).dragsort({
3772 dragSelector: ".handle",
3773 dragEnd: function() {
3774 var $this = $(this);
3775 var oldIndex = $this.data('itemidx-orig');
3776 var newIndex = $this.data('itemidx');
3777 var rows = modules[moduleID].options[optionKey].value;
3778 var row = rows.splice(oldIndex, 1)[0];
3779 rows.splice(newIndex, 0, row);
3782 scrollContainer: this.RESConfigPanelOptions,
3783 placeHolderTemplate: "<tr><td>---</td></tr>",
3784 placeholderTemplate: "<tr><td>---</td></tr>",
3788 if ((thisOptions[i].type === 'text') || (thisOptions[i].type === 'password') || (thisOptions[i].type === 'keycode')) thisOptionDescription.classList.add('textInput');
3789 var thisOptionFormEle = this.drawOptionInput(moduleID, i, thisOptions[i]);
3790 thisOptionContainer.appendChild(thisOptionFormEle);
3791 thisOptionContainer.appendChild(thisOptionDescription);
3793 var thisClear = document.createElement('div');
3794 thisClear.setAttribute('class','clear');
3795 thisOptionContainer.appendChild(thisClear);
3796 allOptionsContainer.appendChild(thisOptionContainer);
3800 if (optCount === 0) {
3801 var noOptions = createElementWithID('div','noOptions');
3802 noOptions.classList.add('optionContainer');
3803 $(noOptions).text('There are no configurable options for this module');
3804 this.RESConfigPanelOptions.appendChild(noOptions);
3806 // var thisSaveStatusBottom = createElementWithID('div','moduleOptionsSaveStatusBottom','saveStatus');
3807 // this.RESConfigPanelOptions.appendChild(thisBottomSaveButton);
3808 // this.RESConfigPanelOptions.appendChild(thisSaveStatusBottom);
3809 this.moduleOptionsScrim = createElementWithID('div','moduleOptionsScrim');
3810 if (modules[moduleID].isEnabled()) {
3811 RESConsole.moduleOptionsScrim.classList.remove('visible');
3812 $('#moduleOptionsSave').fadeIn();
3814 RESConsole.moduleOptionsScrim.classList.add('visible');
3815 $('#moduleOptionsSave').fadeOut();
3817 allOptionsContainer.appendChild(this.moduleOptionsScrim);
3818 // console.log($(thisSaveButton).position());
3821 deleteOptionRow: function(e) {
3822 var thisRow = e.target.parentNode.parentNode;
3823 $(thisRow).remove();
3825 saveCurrentModuleOptions: function(e) {
3827 var panelOptionsDiv = this.RESConfigPanelOptions;
3828 // first, go through inputs that aren't a part of a "table of options"...
3829 var inputs = panelOptionsDiv.querySelectorAll('input, textarea');
3830 for (var i=0, len=inputs.length;i<len;i++) {
3831 // save values of any inputs onscreen, but skip ones with 'capturefor' - those are display only.
3832 var notTokenPrefix = (inputs[i].getAttribute('id') !== null) && (inputs[i].getAttribute('id').indexOf('token-input-') === -1);
3833 if ((notTokenPrefix) && (inputs[i].getAttribute('type') !== 'button') && (inputs[i].getAttribute('displayonly') !== 'true') && (inputs[i].getAttribute('tableOption') !== 'true')) {
3834 // get the option name out of the input field id - unless it's a radio button...
3836 if (inputs[i].getAttribute('type') === 'radio') {
3837 optionName = inputs[i].getAttribute('name');
3839 optionName = inputs[i].getAttribute('id');
3841 // get the module name out of the input's moduleid attribute
3842 var optionValue, moduleID = RESConsole.currentModule;
3843 if (inputs[i].getAttribute('type') === 'checkbox') {
3844 optionValue = !!inputs[i].checked;
3845 } else if (inputs[i].getAttribute('type') === 'radio') {
3846 if (inputs[i].checked) {
3847 optionValue = inputs[i].value;
3850 // check if it's a keycode, in which case we need to parse it into an array...
3851 if ((inputs[i].getAttribute('class')) && (inputs[i].getAttribute('class').indexOf('keycode') !== -1)) {
3852 var tempArray = inputs[i].value.split(',');
3853 // convert the internal values of this array into their respective types (int, bool, bool, bool)
3854 optionValue = [parseInt(tempArray[0], 10), (tempArray[1] === 'true'), (tempArray[2] === 'true'), (tempArray[3] === 'true'), (tempArray[4] === 'true')];
3856 optionValue = inputs[i].value;
3859 if (typeof optionValue !== 'undefined') {
3860 RESUtils.setOption(moduleID, optionName, optionValue);
3864 // Check if there are any tables of options on this panel...
3865 var optionsTables = panelOptionsDiv.querySelectorAll('.optionsTable');
3866 if (typeof optionsTables !== 'undefined') {
3867 // For each table, we need to go through each row in the tbody, and then go through each option and make a multidimensional array.
3868 // For example, something like: [['foo','bar','baz'],['pants','warez','cats']]
3869 for (var i = 0, len = optionsTables.length; i < len; i++) {
3870 var moduleID = optionsTables[i].getAttribute('moduleID');
3871 var optionName = optionsTables[i].getAttribute('optionName');
3872 var thisTBODY = optionsTables[i].querySelector('tbody');
3873 var thisRows = thisTBODY.querySelectorAll('tr');
3874 // check if there are any rows...
3875 if (typeof thisRows !== 'undefined') {
3876 // go through each row, and get all of the inputs...
3877 var optionMulti = [];
3878 var optionRowCount = 0;
3879 for (var j = 0; j < thisRows.length; j++) {
3881 var cells = thisRows[j].querySelectorAll('td.hasTableOption');
3882 var notAllBlank = false;
3883 for (var k = 0; k < cells.length; k++) {
3884 var inputs = cells[k].querySelectorAll('input[tableOption=true], textarea[tableOption=true]');
3885 var optionValue = null;
3886 for (var l = 0; l < inputs.length; l++) {
3887 // get the module name out of the input's moduleid attribute
3888 // var moduleID = inputs[l].getAttribute('moduleID');
3889 if (inputs[l].getAttribute('type') === 'checkbox') {
3890 optionValue = inputs[l].checked;
3891 } else if (inputs[l].getAttribute('type') === 'radio') {
3892 if (inputs[l].checked) {
3893 optionValue = inputs[l].value;
3896 // check if it's a keycode, in which case we need to parse it into an array...
3897 if ((inputs[l].getAttribute('class')) && (inputs[l].getAttribute('class').indexOf('keycode') !== -1)) {
3898 var tempArray = inputs[l].value.split(',');
3899 // convert the internal values of this array into their respective types (int, bool, bool, bool)
3900 optionValue = [parseInt(tempArray[0], 10), (tempArray[1] === 'true'), (tempArray[2] === 'true'), (tempArray[3] === 'true')];
3902 optionValue = inputs[l].value;
3905 if ((optionValue !== '') && (inputs[l].getAttribute('type') !== 'radio')
3906 //If no keyCode is set, then discard the value
3907 && !(Array.isArray(optionValue) && isNaN(optionValue[0]))) {
3910 // optionRow[k] = optionValue;
3912 optionRow.push(optionValue);
3914 // just to be safe, added a check for optionRow !== null...
3915 if ((notAllBlank) && (optionRow !== null)) {
3916 optionMulti[optionRowCount] = optionRow;
3920 if (optionMulti == null) {
3923 // ok, we've got all the rows... set the option.
3924 if (typeof optionValue !== 'undefined') {
3925 RESUtils.setOption(moduleID, optionName, optionMulti);
3931 var statusEle = document.getElementById('moduleOptionsSaveStatus');
3933 $(statusEle).text('Options have been saved...');
3934 statusEle.setAttribute('style','display: block; opacity: 1');
3936 RESUtils.fadeElementOut(statusEle, 0.1);
3937 if (moduleID === 'RESPro') RESStorage.removeItem('RESmodules.RESPro.lastAuthFailed');
3939 drawAboutPanel: function() {
3940 var RESConsoleAboutPanel = this.RESConsoleAboutPanel;
3941 var AboutPanelHTML = ' \
3942 <div id="RESAboutPane"> \
3943 <div id="Button-DonateRES" class="moduleButton active">Donate</div> \
3944 <div id="Button-AboutRES" class="moduleButton">About RES</div> \
3945 <div id="Button-RESTeam" class="moduleButton">About the RES Team</div> \
3946 <div id="Button-SearchRES" class="moduleButton">Search RES Settings</div> \
3948 <div id="RESAboutDetails"> \
3949 <div id="DonateRES" class="aboutPanel"> \
3950 <h3>Contribute to support RES</h3> \
3951 <p>RES is entirely free - as in beer, as in open source, as in everything. If you like our work, a contribution would be greatly appreciated.</p> \
3952 <p>When you contribute, you make it possible for the team to cover hosting costs and other expenses so that we can focus on doing what we do best: making your Reddit experience even better.</p> \
3954 <strong style="font-weight: bold;">Dwolla and bitcoin are the preferred methods of contribution</strong>, because they charge much smaller fees than PayPal and Google: <br><br>\
3955 <a target="_blank" href="https://www.dwolla.com/u/812-686-0217"><img src="https://www.dwolla.com/content/images/btn-donate-with-dwolla.png"></a>\
3958 We also do have PayPal: <br> \
3959 <form action="https://www.paypal.com/cgi-bin/webscr" method="post"><input type="hidden" name="cmd" value="_s-xclick"><input type="hidden" name="hosted_button_id" value="S7TAR7QU39H22"><input type="image" src="https://www.paypalobjects.com/en_US/i/logo/PayPal_mark_60x38.gif" border="0" name="submit" alt="PayPal - The safer, easier way to pay online!"><img alt="" border="0" src="https://www.paypal.com/en_US/i/scr/pixel.gif" width="1" height="1"></form> \
3964 Or Google Checkout: \
3965 <form action="https://checkout.google.com/api/checkout/v2/checkoutForm/Merchant/474530516020369" id="BB_BuyButtonForm" method="post" name="BB_BuyButtonForm" target="_top"> \
3966 <input name="item_name_1" type="hidden" value="Purchase - Reddit Enhancement Suite"/> \
3967 <input name="item_description_1" type="hidden" value="purchase"/> \
3968 <input name="item_quantity_1" type="hidden" value="1"/> \
3969 $<input name="item_price_1" type="text" value="" size="2" /> \
3970 <input name="item_currency_1" type="hidden" value="USD"/> \
3971 <input name="_charset_" type="hidden" value="utf-8"/> \
3972 <input alt="" src="https://checkout.google.com/buttons/buy.gif?merchant_id=474530516020369&w=117&h=48&style=white&variant=text&loc=en_US" type="image"/> \
3977 <form action="https://bitpay.com/checkout" method="post" > \
3978 <input type="hidden" name="action" value="checkout" /> \
3979 <input type="hidden" name="posData" value="" /> \
3980 <input type="hidden" name="data" value="ddFrz5v8dCQ/2oQV9a+OLm3nVrlinxOo1WYFsRjZR5IoouplTgMj7zg8OB3i5xSYiPTbyUmBiNjoY9z/iuEwqPvQClXqQdGINb+IVIzjuZobCUDsLUztc2qdQHvU/sLQzf3a339vzs9JAwSj6W/IpNt90WN1Bdab491xtPpeCIwcS84WY0T+QDjN0c5+1k8/rNqr6A6hFX4TWwZrxQiv35Bl/xyo+YLrE9OUM0K+2cu1hsc7/sOMNsqIB1v4W5CMkQFP40sq9CWf14nyvYbZtg==" /> \
3981 <input type="text" name="price" size="2" value="" /> bitcoins \
3982 <input type="image" src="https://bitpay.com/img/button8.png" border="0" name="submit" alt="BitPay, the easy way to pay with bitcoins." > \
3986 <div id="AboutRES" class="aboutPanel"> \
3987 <h3>About RES</h3> \
3988 <p>Author: <a target="_blank" href="http://www.honestbleeps.com/">honestbleeps</a><br></p> \
3989 <p>Description: Reddit Enhancement Suite is a collection of modules that makes browsing reddit a whole lot easier.</p> \
3990 <p>It\'s built with <a target="_blank" href="http://redditenhancementsuite.com/api">an API</a> that allows you to contribute and include your own modules!</p> \
3991 <p>If you\'ve got bug reports or issues with RES, please see the <a target="_blank" href="http://www.reddit.com/r/RESIssues/">RESIssues</a> subreddit. If you\'d like to follow progress on RES, or you\'d like to converse with other users, please see the <a target="_blank" href="http://www.reddit.com/r/Enhancement/">Enhancement subreddit</a>. You can also check the <a href="http://www.reddit.com/r/Enhancement/wiki/index" target="_blank">wiki</a> for the FAQ, and more detailed info on each module.</p> \
3992 <p>If you want to contribute to the RES code base, submit bug reports, or make suggestions, you can also visit RES at <a href="https://github.com/honestbleeps/Reddit-Enhancement-Suite" target="_blank">Github</a>.</p> \
3993 <p>If you want to contact me directly with suggestions, bug reports or just want to say you appreciate the work, an <a href="mailto:steve@honestbleeps.com">email</a> would be great.</p> \
3994 <p>License: Reddit Enhancement Suite is released under the <a target="_blank" href="http://www.gnu.org/licenses/gpl-3.0.html">GPL v3.0</a>.</p> \
3995 <p><strong>Note:</strong> 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.</p> \
3997 <div id="RESTeam" class="aboutPanel"> \
3998 <h3>About the RES Team</h3> \
3999 <p>Steve Sobel (<a target="_blank" href="http://www.reddit.com/user/honestbleeps/">honestbleeps</a>) 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 <a target="_blank" href="http://redditenhancementsuite.com/about.html">the RES website.</a></p> \
4001 <div id="SearchRES" class="aboutPanel"></div> \
4004 $(RESConsoleAboutPanel).html(AboutPanelHTML);
4005 var searchPanel = modules['settingsNavigation'].renderSearchPanel();
4006 $('#SearchRES', RESConsoleAboutPanel).append(searchPanel);
4007 $(RESConsoleAboutPanel).find('.moduleButton').click(function(e) {
4008 $('.moduleButton').removeClass('active');
4009 $(this).addClass('active');
4010 var thisID = $(this).attr('id');
4011 var thisPanel = thisID.replace('Button-','');
4012 var visiblePanel = $(this).parent().parent().find('.aboutPanel:visible');
4014 var duration = (e.data && e.data.duration) || $(this).hasClass('active') ? 0 : 400;
4015 $(visiblePanel).fadeOut(duration, function () {
4016 $('#'+thisPanel).fadeIn();
4019 this.RESConsoleContent.appendChild(RESConsoleAboutPanel);
4021 drawProPanel: function() {
4022 RESConsoleProPanel = this.RESConsoleProPanel;
4023 var proPanelHeader = document.createElement('div');
4024 $(proPanelHeader).html('RES Pro allows you to save your preferences to the RES Pro server.<br><br><strong>Please note:</strong> this is beta functionality right now. Please don\'t consider this to be a "backup" solution just yet. To start, you will need to <a target="_blank" href="http://redditenhancementsuite.com/register.php">register for a PRO account</a> first, then email <a href="mailto:steve@honestbleeps.com">steve@honestbleeps.com</a> with your RES Pro username to get access.');
4025 RESConsoleProPanel.appendChild(proPanelHeader);
4026 this.proSetupButton = createElementWithID('div','RESProSetup');
4027 this.proSetupButton.setAttribute('class','RESButton');
4028 $(this.proSetupButton).text('Configure RES Pro');
4029 this.proSetupButton.addEventListener('click', function(e) {
4031 modules['RESPro'].configure();
4033 RESConsoleProPanel.appendChild(this.proSetupButton);
4035 this.proAuthButton = createElementWithID('div','RESProAuth');
4036 this.proAuthButton.setAttribute('class','RESButton');
4037 $(this.proAuthButton).html('Authenticate');
4038 this.proAuthButton.addEventListener('click', function(e) {
4040 modules['RESPro'].authenticate();
4042 RESConsoleProPanel.appendChild(this.proAuthButton);
4044 this.proSaveButton = createElementWithID('div','RESProSave');
4045 this.proSaveButton.setAttribute('class','RESButton');
4046 $(this.proSaveButton).text('Save Module Options');
4047 this.proSaveButton.addEventListener('click', function(e) {
4049 // modules['RESPro'].savePrefs();
4050 modules['RESPro'].authenticate(modules['RESPro'].savePrefs());
4052 RESConsoleProPanel.appendChild(this.proSaveButton);
4055 this.proUserTaggerSaveButton = createElementWithID('div','RESProSave');
4056 this.proUserTaggerSaveButton.setAttribute('class','RESButton');
4057 $(this.proUserTaggerSaveButton).html('Save user tags to Server');
4058 this.proUserTaggerSaveButton.addEventListener('click', function(e) {
4060 modules['RESPro'].saveModuleData('userTagger');
4062 RESConsoleProPanel.appendChild(this.proUserTaggerSaveButton);
4065 this.proSaveCommentsSaveButton = createElementWithID('div','RESProSaveCommentsSave');
4066 this.proSaveCommentsSaveButton.setAttribute('class','RESButton');
4067 $(this.proSaveCommentsSaveButton).text('Save saved comments to Server');
4068 this.proSaveCommentsSaveButton.addEventListener('click', function(e) {
4070 // modules['RESPro'].saveModuleData('saveComments');
4071 modules['RESPro'].authenticate(modules['RESPro'].saveModuleData('saveComments'));
4073 RESConsoleProPanel.appendChild(this.proSaveCommentsSaveButton);
4075 this.proSubredditManagerSaveButton = createElementWithID('div','RESProSubredditManagerSave');
4076 this.proSubredditManagerSaveButton.setAttribute('class','RESButton');
4077 $(this.proSubredditManagerSaveButton).text('Save subreddits to server');
4078 this.proSubredditManagerSaveButton.addEventListener('click', function(e) {
4080 // modules['RESPro'].saveModuleData('SubredditManager');
4081 modules['RESPro'].authenticate(modules['RESPro'].saveModuleData('subredditManager'));
4083 RESConsoleProPanel.appendChild(this.proSubredditManagerSaveButton);
4085 this.proSaveCommentsGetButton = createElementWithID('div','RESProGetSavedComments');
4086 this.proSaveCommentsGetButton.setAttribute('class','RESButton');
4087 $(this.proSaveCommentsGetButton).text('Get saved comments from Server');
4088 this.proSaveCommentsGetButton.addEventListener('click', function(e) {
4090 // modules['RESPro'].getModuleData('saveComments');
4091 modules['RESPro'].authenticate(modules['RESPro'].getModuleData('saveComments'));
4093 RESConsoleProPanel.appendChild(this.proSaveCommentsGetButton);
4095 this.proSubredditManagerGetButton = createElementWithID('div','RESProGetSubredditManager');
4096 this.proSubredditManagerGetButton.setAttribute('class','RESButton');
4097 $(this.proSubredditManagerGetButton).text('Get subreddits from Server');
4098 this.proSubredditManagerGetButton.addEventListener('click', function(e) {
4100 // modules['RESPro'].getModuleData('SubredditManager');
4101 modules['RESPro'].authenticate(modules['RESPro'].getModuleData('subredditManager'));
4103 RESConsoleProPanel.appendChild(this.proSubredditManagerGetButton);
4105 this.proGetButton = createElementWithID('div','RESProGet');
4106 this.proGetButton.setAttribute('class','RESButton');
4107 $(this.proGetButton).text('Get options from Server');
4108 this.proGetButton.addEventListener('click', function(e) {
4110 // modules['RESPro'].getPrefs();
4111 modules['RESPro'].authenticate(modules['RESPro'].getPrefs());
4113 RESConsoleProPanel.appendChild(this.proGetButton);
4114 this.RESConsoleContent.appendChild(RESConsoleProPanel);
4116 open: function(moduleIdOrCategory) {
4117 var category, moduleID;
4118 if (moduleIdOrCategory === 'search') {
4119 moduleID = moduleIdOrCategory;
4120 category = 'About RES';
4122 var module = modules[moduleIdOrCategory];
4123 moduleID = module && module.moduleID;
4124 category = module && module.category;
4126 category = category || moduleIdOrCategory || this.categories[0];
4127 moduleID = moduleID || this.getModuleIDsByCategory(category)[0];
4130 // Draw the config panel
4131 this.drawConfigPanel();
4132 // Draw the about panel
4133 this.drawAboutPanel();
4134 // Draw the RES Pro panel
4135 // this.drawProPanel();
4136 this.openCategoryPanel(category);
4137 this.showConfigOptions(moduleID);
4140 // 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!
4141 var adFrame = document.getElementById('ad-frame');
4142 if ((typeof adFrame !== 'undefined') && (adFrame !== null)) {
4143 adFrame.style.display = 'none';
4145 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'RESConsole');
4146 // var leftCentered = Math.floor((window.innerWidth - 720) / 2);
4147 // modalOverlay.setAttribute('style','display: block; height: ' + document.documentElement.scrollHeight + 'px');
4148 this.modalOverlay.classList.remove('fadeOut');
4149 this.modalOverlay.classList.add('fadeIn');
4151 // this.RESConsoleContainer.setAttribute('style','display: block; left: ' + leftCentered + 'px');
4152 // this.RESConsoleContainer.setAttribute('style','display: block; left: 1.5%;');
4153 this.RESConsoleContainer.classList.remove('slideOut');
4154 this.RESConsoleContainer.classList.add('slideIn');
4156 RESStorage.setItem('RESConsole.hasOpenedConsole', true);
4158 document.body.addEventListener('keyup', RESConsole.handleEscapeKey, false);
4160 handleEscapeKey: function(e) {
4161 // don't close if the user is in a token input field (e.g. adding subreddits to a list)
4162 // because they probably just want to cancel the dropdown list
4163 if (e.keyCode === 27 && (document.activeElement.id.indexOf('token-input') === -1)) {
4165 document.body.removeEventListener('keyup', RESConsole.handleEscapeKey, false);
4169 $('#moduleOptionsSave').fadeOut();
4170 this.isOpen = false;
4171 // Let's be nice to reddit and put their ad frame back now...
4172 var adFrame = document.getElementById('ad-frame');
4173 if ((typeof adFrame !== 'undefined') && (adFrame !== null)) {
4174 adFrame.style.display = 'block';
4177 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'RESConsole');
4179 // this.RESConsoleContainer.setAttribute('style','display: none;');
4180 this.modalOverlay.classList.remove('fadeIn');
4181 this.modalOverlay.classList.add('fadeOut');
4182 this.RESConsoleContainer.classList.remove('slideIn');
4183 this.RESConsoleContainer.classList.add('slideOut');
4184 // just in case the user was in the middle of setting a key and decided to close the dialog, clean that up.
4185 if (typeof RESConsole.keyCodeModal !== 'undefined') {
4186 RESConsole.keyCodeModal.style.display = 'none';
4187 RESConsole.captureKey = false;
4190 modules['settingsNavigation'].resetUrlHash();
4192 menuClick: function(obj) {
4195 var objID = obj.getAttribute('id');
4196 var category = objID.split('-'); category = category[category.length - 1];
4197 var moduleID = this.getModuleIDsByCategory(category)[0];
4198 this.openCategoryPanel(category);
4199 this.showConfigOptions(moduleID);
4201 openCategoryPanel: function(category) {
4202 // make all menu items look unselected
4203 $(RESConsole.RESMenuItems).removeClass('active');
4205 // make selected menu item look selected
4206 $(RESConsole.RESMenuItems).filter(function() {
4207 var thisCategory = (this.getAttribute('id') || '').split('-');
4208 thisCategory = thisCategory[thisCategory.length - 1];
4210 if (thisCategory == category) return true;
4211 }).addClass('active');
4213 // hide all console panels
4214 $(RESConsole.RESConsoleContent).find('.RESPanel').hide();
4217 case 'Menu-About RES': // cruft
4219 // show the about panel
4220 $(this.RESConsoleAboutPanel).show();
4222 case 'Menu-RES Pro': // cruft
4224 // show the pro panel
4225 $(this.RESConsoleProPanel).show();
4228 // show the config panel for the given category
4229 $(this.RESConsoleConfigPanel).show();
4230 this.drawConfigPanelCategory(category);
4237 /************************************************************************************************************
4239 Creating your own module:
4241 Modules must have the following format, with required functions:
4242 - moduleID - the name of the module, i.e. myModule
4243 - moduleName - a "nice name" for your module...
4244 - description - for the config panel, explains what the module is
4245 - isEnabled - should always return RESConsole.getModulePrefs('moduleID') - where moduleID is your module name.
4246 - isMatchURL - should always return RESUtils.isMatchURL('moduleID') - checks your include and exclude URL matches.
4247 - include - an array of regexes to match against location.href (basically like include in GM)
4248 - exclude (optional) - an array of regexes to exclude against location.href
4249 - go - always checks both if isEnabled() and if RESUtils.isMatchURL(), and if so, runs your main code.
4251 modules['myModule'] = {
4252 moduleID: 'myModule',
4253 moduleName: 'my module',
4254 category: 'CategoryName',
4256 // any configurable options you have go here...
4257 // options must have a type and a value..
4258 // valid types are: text, boolean (if boolean, value must be true or false)
4262 value: 'this is default text',
4263 description: 'explanation of what this option is for'
4268 description: 'explanation of what this option is for'
4271 description: 'This is my module!',
4272 isEnabled: function() {
4273 return RESConsole.getModulePrefs(this.moduleID);
4276 /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.]+/i,
4277 /^https?:\/\/([a-z]+)\.reddit\.com\/message\/comments\/[-\w\.]+/i
4279 isMatchURL: function() {
4280 return RESUtils.isMatchURL(this.moduleID);
4283 if ((this.isEnabled()) && (this.isMatchURL())) {
4285 // this is where your code goes...
4288 }; // note: you NEED this semicolon at the end!
4290 ************************************************************************************************************/
4293 modules['subRedditTagger'] = {
4294 moduleID: 'subRedditTagger',
4295 moduleName: 'Subreddit Tagger',
4296 category: 'Filters',
4300 addRowText: '+add tag',
4302 { name: 'subreddit', type: 'text' },
4303 { name: 'doesntContain', type: 'text' },
4304 { name: 'tag', type: 'text' }
4308 ['somebodymakethis','SMT','[SMT]'],
4309 ['pics','pic','[pic]']
4312 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.'
4315 description: 'Adds tags to posts on subreddits (i.e. [SMT] on SomebodyMakeThis when the user leaves it out)',
4316 isEnabled: function() {
4317 return RESConsole.getModulePrefs(this.moduleID);
4320 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
4322 isMatchURL: function() {
4323 return RESUtils.isMatchURL(this.moduleID);
4326 if ((this.isEnabled()) && (this.isMatchURL())) {
4327 this.checkForOldSettings();
4328 this.SRTDoesntContain = [];
4329 this.SRTTagWith = [];
4330 this.loadSRTRules();
4332 RESUtils.watchForElement('siteTable', modules['subRedditTagger'].scanTitles);
4337 loadSRTRules: function () {
4338 var subReddits = this.options.subReddits.value;
4339 for (var i=0, len=subReddits.length; i<len; i++) {
4340 var thisGetArray = subReddits[i];
4342 modules['subRedditTagger'].SRTDoesntContain[thisGetArray[0].toLowerCase()] = thisGetArray[1];
4343 modules['subRedditTagger'].SRTTagWith[thisGetArray[0].toLowerCase()] = thisGetArray[2];
4347 scanTitles: function(obj) {
4348 var qs = '#siteTable > .thing > DIV.entry';
4350 qs = '.thing > DIV.entry';
4354 var entries = obj.querySelectorAll(qs);
4355 for (var i=0, len=entries.length; i<len;i++) {
4356 var thisSubRedditEle = entries[i].querySelector('A.subreddit');
4357 if ((typeof thisSubRedditEle !== 'undefined') && (thisSubRedditEle !== null)) {
4358 var thisSubReddit = thisSubRedditEle.innerHTML.toLowerCase();
4359 if (typeof modules['subRedditTagger'].SRTTagWith[thisSubReddit] !== 'undefined') {
4360 if (thisSubReddit && !modules['subRedditTagger'].SRTDoesntContain[thisSubReddit]) {
4361 modules['subRedditTagger'].SRTDoesntContain[thisSubReddit] = '['+thisSubReddit+']';
4363 var thisTitle = entries[i].querySelector('a.title');
4364 if (!thisTitle.classList.contains('srTagged')) {
4365 thisTitle.classList.add('srTagged');
4366 var thisString = modules['subRedditTagger'].SRTDoesntContain[thisSubReddit];
4367 var thisTagWith = modules['subRedditTagger'].SRTTagWith[thisSubReddit];
4368 if (thisTitle.text.indexOf(thisString) === -1) {
4369 $(thisTitle).text(escapeHTML(thisTagWith) + ' ' + $(thisTitle).text());
4376 checkForOldSettings: function() {
4377 var settingsCopy = [];
4378 var subRedditCount = 0;
4379 while (RESStorage.getItem('subreddit_' + subRedditCount)) {
4380 var thisGet = RESStorage.getItem('subreddit_' + subRedditCount).replace(/\"/g,"");
4381 var thisGetArray = thisGet.split("|");
4382 settingsCopy[subRedditCount] = thisGetArray;
4383 RESStorage.removeItem('subreddit_' + subRedditCount);
4386 if (subRedditCount > 0) {
4387 RESUtils.setOption('subRedditTagger', 'subReddits', settingsCopy);
4393 modules['uppersAndDowners'] = {
4394 moduleID: 'uppersAndDowners',
4395 moduleName: 'Uppers and Downers Enhanced',
4401 description: 'Show +/- signs next to upvote/downvote tallies.'
4406 description: 'Uppers and Downers on links.'
4410 value: 'color:rgb(255, 139, 36); font-weight:normal;',
4411 description: 'CSS style for post upvotes'
4413 postDownvoteStyle: {
4415 value: 'color:rgb(148, 148, 255); font-weight:normal;',
4416 description: 'CSS style for post upvotes'
4418 commentUpvoteStyle: {
4420 value: 'color:rgb(255, 139, 36); font-weight:bold;',
4421 description: 'CSS style for comment upvotes'
4423 commentDownvoteStyle: {
4425 value: 'color:rgb(148, 148, 255); font-weight:bold;',
4426 description: 'CSS style for comment upvotes'
4431 description: 'Force upvote/downvote counts to be visible (when subreddit CSS tries to hide them)'
4434 description: 'Displays up/down vote counts on comments.',
4435 isEnabled: function() {
4436 return RESConsole.getModulePrefs(this.moduleID);
4439 /^https?:\/\/([a-z]+)\.reddit\.com\/?(?:\??[\w]+=[\w]+&?)*/i,
4440 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\w]+\/?(?:\??[\w]+=[\w]+&?)*$/i,
4441 /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.]+/i,
4442 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/?[-\w\.]*/i,
4443 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
4445 isMatchURL: function() {
4446 return RESUtils.isMatchURL(this.moduleID);
4448 beforeLoad: function() {
4449 if ((this.isEnabled()) && (this.isMatchURL())) {
4450 // added code to force inline-block and opacity: 1 to prevent CSS from hiding .res_* classes...
4451 var forceVisible = (this.options.forceVisible.value) ? '; visibility: visible !important; opacity: 1 !important; display: inline-block !important;' : '';
4452 var css = '.res_comment_ups { '+this.options.commentUpvoteStyle.value+forceVisible+' } .res_comment_downs { '+this.options.commentDownvoteStyle.value+forceVisible+' }';
4453 css += '.res_post_ups { '+this.options.postUpvoteStyle.value+forceVisible+' } .res_post_downs { '+this.options.postDownvoteStyle.value+forceVisible+' }';
4454 RESUtils.addCSS(css);
4458 if ((this.isEnabled()) && (this.isMatchURL())) {
4459 // get rid of the showTimeStamp options since Reddit now has this feature natively.
4460 if (typeof this.options.showTimestamp !== 'undefined') {
4461 delete this.options.showTimestamp;
4462 RESStorage.setItem('RESoptions.uppersAndDowners', JSON.stringify(modules['uppersAndDowners'].options));
4464 if (RESUtils.pageType() === 'comments') {
4465 this.commentsWithMoos = [];
4466 this.moreCommentsIDs = [];
4467 this.applyUppersAndDownersToComments();
4468 RESUtils.watchForElement('newComments', modules['uppersAndDowners'].applyUppersAndDownersToComments);
4469 } else if (RESUtils.pageType() === 'profile') {
4470 this.commentsWithMoos = [];
4471 this.moreCommentsIDs = [];
4472 this.applyUppersAndDownersToMixed();
4473 RESUtils.watchForElement('siteTable', modules['uppersAndDowners'].applyUppersAndDownersToMixed);
4475 } else if ((RESUtils.pageType() === 'linklist') && (this.options.applyToLinks.value)) {
4476 this.linksWithMoos = [];
4477 this.applyUppersAndDownersToLinks();
4478 RESUtils.watchForElement('siteTable', modules['uppersAndDowners'].applyUppersAndDownersToLinks);
4482 applyUppersAndDownersToComments: function(ele) {
4484 ele = document.body;
4486 if (ele.classList.contains('comment')) {
4487 modules['uppersAndDowners'].showUppersAndDownersOnComment(ele);
4488 } else if (ele.classList.contains('entry')) {
4489 modules['uppersAndDowners'].showUppersAndDownersOnComment(ele.parentNode);
4491 var allComments = ele.querySelectorAll('div.comment');
4492 RESUtils.forEachChunked(allComments, 15, 1000, function(comment, i, array) {
4493 modules['uppersAndDowners'].showUppersAndDownersOnComment(comment);
4497 applyUppersAndDownersToMixed: function(ele) {
4498 ele = ele || document.body;
4499 var linkList = ele.querySelectorAll('div.thing.link, div.thing.comment'),
4500 displayType = 'regular',
4501 thisPlus, thisMinus;
4502 if (modules['uppersAndDowners'].options.showSigns.value) {
4509 for (var i=0, len=linkList.length; i<len; i++) {
4510 if (linkList[i].classList.contains('link')) {
4511 var thisups = linkList[i].getAttribute('data-ups');
4512 var thisdowns = linkList[i].getAttribute('data-downs');
4514 var thisTagline = linkList[i].querySelector('p.tagline');
4515 // Check if compressed link display or regular...
4516 if ((typeof thisTagline !== 'undefined') && (thisTagline !== null)) {
4517 var upsAndDownsEle = $("<span> (<span class='res_post_ups'>"+thisPlus+thisups+"</span>|<span class='res_post_downs'>"+thisMinus+thisdowns+"</span>) </span>");
4518 if (displayType === 'regular') {
4519 // thisTagline.insertBefore(upsAndDownsEle, thisTagline.firstChild);
4520 $(thisTagline).prepend(upsAndDownsEle);
4522 $(thisTagline).after(upsAndDownsEle);
4526 modules['uppersAndDowners'].showUppersAndDownersOnComment(linkList[i]);
4531 showUppersAndDownersOnComment: function(commentEle) {
4532 // if this is not a valid comment (e.g. a load more comments div, which has the same classes for some reason)
4533 if ((commentEle.getAttribute('data-votesvisible') === 'true')
4534 || (commentEle.classList.contains('morechildren'))
4535 || (commentEle.classList.contains('morerecursion'))
4536 || (commentEle.classList.contains('score-hidden'))) {
4539 commentEle.setAttribute('data-votesvisible', 'true');
4540 var tagline = commentEle.querySelector('p.tagline');
4541 var ups = commentEle.getAttribute('data-ups');
4542 var downs = commentEle.getAttribute('data-downs');
4543 var openparen, closeparen, mooups, moodowns, voteUps, voteDowns, pipe;
4544 var frag = document.createDocumentFragment(); //using a fragment speeds this up by a factor of about 2
4547 if (modules['uppersAndDowners'].options.showSigns.value) {
4552 openparen = document.createTextNode(" (");
4553 frag.appendChild(openparen);
4555 mooups = document.createElement("span");
4556 mooups.className = "res_comment_ups";
4557 voteUps = document.createTextNode(ups);
4559 mooups.appendChild(voteUps);
4560 frag.appendChild(mooups);
4562 pipe = document.createTextNode("|");
4563 tagline.appendChild(pipe);
4565 moodowns = document.createElement("span");
4566 moodowns.className = "res_comment_downs";
4568 voteDowns = document.createTextNode(downs);
4569 moodowns.appendChild(voteDowns);
4571 frag.appendChild(moodowns);
4573 closeparen = document.createTextNode(")");
4574 frag.appendChild(closeparen);
4576 frag.appendChild(openparen);
4577 frag.appendChild(mooups);
4578 frag.appendChild(pipe);
4579 frag.appendChild(moodowns);
4580 frag.appendChild(closeparen);
4582 tagline.appendChild(frag);
4584 applyUppersAndDownersToLinks: function(ele) {
4585 // Since we're dealing with max 100 links at a time, we don't need a chunker here...
4586 ele = ele || document.body;
4587 var linkList = ele.querySelectorAll('div.thing.link'),
4588 displayType = 'regular',
4589 thisPlus, thisMinus;
4590 if (modules['uppersAndDowners'].options.showSigns.value) {
4597 for (var i=0, len=linkList.length; i<len; i++) {
4598 var thisups = linkList[i].getAttribute('data-ups');
4599 var thisdowns = linkList[i].getAttribute('data-downs');
4601 var thisTagline = linkList[i].querySelector('p.tagline');
4602 // Check if compressed link display or regular...
4603 if ((typeof thisTagline !== 'undefined') && (thisTagline !== null)) {
4604 var upsAndDownsEle = $("<span> (<span class='res_post_ups'>"+thisPlus+thisups+"</span>|<span class='res_post_downs'>"+thisMinus+thisdowns+"</span>) </span>");
4605 if (displayType === 'regular') {
4606 // thisTagline.insertBefore(upsAndDownsEle, thisTagline.firstChild);
4607 $(thisTagline).prepend(upsAndDownsEle);
4609 $(thisTagline).after(upsAndDownsEle);
4616 modules['keyboardNav'] = {
4617 moduleID: 'keyboardNav',
4618 moduleName: 'Keyboard Navigation',
4621 // any configurable options you have go here...
4622 // options must have a type and a value..
4623 // valid types are: text, boolean (if boolean, value must be true or false)
4628 description: 'Background color of focused element'
4633 description: 'border style (e.g. 1px dashed gray) for focused element'
4635 focusBGColorNight: {
4638 description: 'Background color of focused element in Night Mode'
4640 focusFGColorNight: {
4643 description: 'Foreground color of focused element in Night Mode'
4648 description: 'border style (e.g. 1px dashed gray) for focused element'
4650 autoSelectOnScroll: {
4653 description: 'Automatically select the topmost element for keyboard navigation on window scroll'
4658 description: 'Scroll window to top of link when expando key is used (to keep pics etc in view)'
4663 { name: 'directional', value: 'directional' },
4664 { name: 'page up/down', value: 'page' },
4665 { name: 'lock to top', value: 'top' }
4667 value: 'directional',
4668 description: 'When moving up/down with keynav, when and how should RES scroll the window?'
4670 commentsLinkNumbers: {
4673 description: 'Assign number keys (e.g. [1]) to links within selected comment'
4675 commentsLinkNumberPosition: {
4678 { name: 'Place on right', value: 'right' },
4679 { name: 'Place on left', value: 'left' }
4682 description: 'Which side commentsLinkNumbers are displayed'
4684 commentsLinkNewTab: {
4687 description: 'Open number key links in a new tab'
4692 description: 'Move keyboard focus to a link or comment when clicked with the mouse'
4697 description: 'After hiding a link, automatically select the next link'
4702 description: 'After voting on a link, automatically select the next link'
4706 value: [191, false, false, true], // ? (note the true in the shift slot)
4707 description: 'Show help for keyboard shortcuts'
4711 value: [190, false, false, false], // .
4712 description: 'Show/hide commandline box'
4716 value: [72, false, false, false], // h
4717 description: 'Hide link'
4721 value: [75, false, false, false], // k
4722 description: 'Move up (previous link or comment)'
4726 value: [74, false, false, false], // j
4727 description: 'Move down (next link or comment)'
4731 value: [75, false, false, true], // shift-k
4732 description: 'Move to top of list (on link pages)'
4736 value: [74, false, false, true], // shift-j
4737 description: 'Move to bottom of list (on link pages)'
4741 value: [75, false, false, true], // shift-k
4742 description: 'Move to previous sibling (in comments) - skips to previous sibling at the same depth.'
4746 value: [74, false, false, true], // shift-j
4747 description: 'Move to next sibling (in comments) - skips to next sibling at the same depth.'
4751 value: [75, true, false, true], // shift-alt-k
4752 description: 'Move to the topmost comment of the previous thread (in comments).'
4756 value: [74, true, false, true], // shift-alt-j
4757 description: 'Move to the topmost comment of the next thread (in comments).'
4761 value: [84, false, false, false], // t
4762 description: 'Move to the topmost comment of the current thread (in comments).'
4766 value: [80, false, false, false], // p
4767 description: 'Move to parent (in comments).'
4771 value: [80, false, false, true], // p
4772 description: 'Display parent comments.'
4776 value: [13, false, false, false], // enter
4777 description: 'Follow link (hold shift to open it in a new tab) (link pages only)'
4781 value: [13, false, false, true], // shift-enter
4782 description: 'Follow link in new tab (link pages only)'
4784 followLinkNewTabFocus: {
4787 description: 'When following a link in new tab - focus the tab?'
4791 value: [88, false, false, false], // x
4792 description: 'Toggle expando (image/text/video) (link pages only)'
4796 value: [187, false, false, false],
4797 description: 'Increase the size of image(s) in the highlighted post area'
4801 value: [189, false, false, false],
4802 description: 'Increase the size of image(s) in the highlighted post area'
4806 value: [187, false, false, true],
4807 description: 'Increase the size of image(s) in the highlighted post area (finer control)'
4809 imageSizeDownFine: {
4811 value: [189, false, false, true],
4812 description: 'Increase the size of image(s) in the highlighted post area (finer control)'
4814 previousGalleryImage: {
4816 value: [219, false, false, false], //[
4817 description: 'View the previous image of an inline gallery.'
4821 value: [221, false, false, false], //]
4822 description: 'View the next image of an inline gallery.'
4826 value: [88, false, false, true], // shift-x
4827 description: 'Toggle "view images" button'
4831 value: [13, false, false, false], // enter
4832 description: 'Expand/collapse comments (comments pages only)'
4836 value: [67, false, false, false], // c
4837 description: 'View comments for link (shift opens them in a new tab)'
4839 followCommentsNewTab: {
4841 value: [67, false, false, true], // shift-c
4842 description: 'View comments for link in a new tab'
4844 followLinkAndCommentsNewTab: {
4846 value: [76, false, false, false], // l
4847 description: 'View link and comments in new tabs'
4849 followLinkAndCommentsNewTabBG: {
4851 value: [76, false, false, true], // shift-l
4852 description: 'View link and comments in new background tabs'
4856 value: [65, false, false, false], // a
4857 description: 'Upvote selected link or comment'
4861 value: [90, false, false, false], // z
4862 description: 'Downvote selected link or comment'
4866 value: [83, false, false, false], // s
4867 description: 'Save the current link'
4871 value: [82, false, false, false], // r
4872 description: 'Reply to current comment (comment pages only)'
4876 value: [69, false, true, false], // control-e
4877 description: 'Open the current markdown field in the big editor. (Only when a markdown form is focused)'
4881 value: [82, false, false, false], // r
4882 description: 'Go to subreddit of selected link (link pages only)'
4884 followSubredditNewTab: {
4886 value: [82, false, false, true], // shift-r
4887 description: 'Go to subreddit of selected link in a new tab (link pages only)'
4891 value: [73, false, false, false], // i
4892 description: 'Go to inbox'
4896 value: [73, false, false, true], // shift+i
4897 description: 'Go to inbox in a new tab'
4901 value: [85, false, false, false], // u
4902 description: 'Go to profile'
4906 value: [85, false, false, true], // shift+u
4907 description: 'Go to profile in a new tab'
4911 value: [70, false, false, false], // f
4912 description: 'Go to front page'
4914 subredditFrontPage: {
4916 value: [70, false, false, true], // shift-f
4917 description: 'Go to subreddit front page'
4921 value: [78, false, false, false], // n
4922 description: 'Go to next page (link list pages only)'
4926 value: [80, false, false, false], // p
4927 description: 'Go to prev page (link list pages only)'
4931 value: [49, false, false, false], // 1
4932 description: 'Open first link within comment.',
4937 value: [50, false, false, false], // 2
4938 description: 'Open link #2 within comment.',
4943 value: [51, false, false, false], // 3
4944 description: 'Open link #3 within comment.',
4949 value: [52, false, false, false], // 4
4950 description: 'Open link #4 within comment.',
4955 value: [53, false, false, false], // 5
4956 description: 'Open link #5 within comment.',
4961 value: [54, false, false, false], // 6
4962 description: 'Open link #6 within comment.',
4967 value: [55, false, false, false], // 7
4968 description: 'Open link #7 within comment.',
4973 value: [56, false, false, false], // 8
4974 description: 'Open link #8 within comment.',
4979 value: [57, false, false, false], // 9
4980 description: 'Open link #9 within comment.',
4985 value: [48, false, false, false], // 0
4986 description: 'Open link #10 within comment.',
4990 description: 'Keyboard navigation for reddit!',
4991 isEnabled: function() {
4992 return RESConsole.getModulePrefs(this.moduleID);
4995 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
4997 isMatchURL: function() {
4998 return RESUtils.isMatchURL(this.moduleID);
5000 beforeLoad: function() {
5001 if ((this.isEnabled()) && (this.isMatchURL())) {
5002 var focusFGColorNight, focusBGColor, focusBGColorNight;
5003 if (typeof this.options.focusBGColor === 'undefined') {
5004 focusBGColor = '#F0F3FC';
5006 focusBGColor = this.options.focusBGColor.value;
5008 var borderType = 'outline';
5009 if (BrowserDetect.isOpera()) borderType = 'border';
5010 if (typeof this.options.focusBorder === 'undefined') {
5013 focusBorder = borderType+': ' + this.options.focusBorder.value + ';';
5015 if (!(this.options.focusBGColorNight.value)) {
5016 focusBGColorNight = '#666';
5018 focusBGColorNight = this.options.focusBGColorNight.value;
5020 if (!(this.options.focusFGColorNight.value)) {
5021 focusFGColorNight = '#DDD';
5023 focusFGColorNight = this.options.focusFGColorNight.value;
5025 if (typeof this.options.focusBorderNight === 'undefined') {
5026 focusBorderNight = '';
5028 focusBorderNight = borderType+': ' + this.options.focusBorderNight.value + ';';
5030 // old style: .RES-keyNav-activeElement { '+borderType+': '+focusBorder+'; background-color: '+focusBGColor+'; } \
5031 // 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
5032 // overlay over the sidebar... yikes.
5033 // .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; } \
5035 // why !important on .RES-keyNav-activeElement? Because some subreddits are unfortunately using !important for no good reason on .entry divs...
5037 .entry { padding-right: 5px; } \
5038 .RES-keyNav-activeElement, .commentarea .RES-keyNav-activeElement .md, .commentarea .RES-keyNav-activeElement.entry .noncollapsed { background-color: '+focusBGColor+' !important; } \
5039 .RES-keyNav-activeElement { '+focusBorder+' } \
5040 .res-nightmode .RES-keyNav-activeElement { '+focusBorderNight+' } \
5041 .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;} \
5042 .res-nightmode .RES-keyNav-activeElement a.title:first-of-type {color: ' + focusFGColorNight + ' !important; } \
5043 #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; } \
5044 #keyHelp th { font-weight: bold; padding: 2px; border-bottom: 1px dashed #ddd; } \
5045 #keyHelp td { padding: 2px; border-bottom: 1px dashed #ddd; } \
5046 #keyHelp td:first-child { width: 70px; } \
5047 #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; } \
5048 #keyCommandInput { width: 240px; background-color: #999; margin-right: 10px; } \
5049 #keyCommandInputTip { margin-top: 5px; color: #9F9; } \
5050 #keyCommandInputTip ul { font-size: 11px; list-style-type: disc; } \
5051 #keyCommandInputTip li { margin-left: 15px; } \
5052 #keyCommandInputError { margin-top: 5px; color: red; font-weight: bold; } \
5053 .keyNavAnnotation { font-size: 9px; position: relative; top: -6px; } \
5058 if ((this.isEnabled()) && (this.isMatchURL())) {
5059 // get rid of antequated option we've removed
5060 this.keyboardNavLastIndexCache = safeJSON.parse(RESStorage.getItem('RESmodules.keyboardNavLastIndex'), false, true);
5061 var idx, now = new Date().getTime();
5062 if (! this.keyboardNavLastIndexCache) {
5063 // this is a one time function to delete old keyboardNavLastIndex junk.
5064 this.keyboardNavLastIndexCache = {};
5065 for (idx in RESStorage) {
5066 if (idx.match(/keyboardNavLastIndex/)) {
5067 var url = idx.replace('RESmodules.keyboardNavLastIndex.','');
5068 this.keyboardNavLastIndexCache[url] = {
5069 index: RESStorage[idx],
5072 RESStorage.removeItem(idx);
5075 this.keyboardNavLastIndexCache.lastScan = now;
5076 RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
5078 // clean cache every 6 hours - delete any urls that haven't been visited in an hour.
5079 if ((typeof this.keyboardNavLastIndexCache.lastScan === 'undefined') || (now - this.keyboardNavLastIndexCache.lastScan > 21600000)) {
5080 for (idx in this.keyboardNavLastIndexCache) {
5081 if ((typeof this.keyboardNavLastIndexCache[idx] === 'object') && (now - this.keyboardNavLastIndexCache[idx].updated > 3600000)) {
5082 delete this.keyboardNavLastIndexCache[idx];
5085 this.keyboardNavLastIndexCache.lastScan = now;
5086 RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
5090 if (this.options.autoSelectOnScroll.value) {
5091 window.addEventListener('scroll', modules['keyboardNav'].handleScroll, false);
5093 if (typeof this.options.scrollTop !== 'undefined') {
5094 if (this.options.scrollTop.value) this.options.scrollStyle.value = 'top';
5095 delete this.options.scrollTop;
5096 RESStorage.setItem('RESoptions.keyboardNav', JSON.stringify(modules['keyboardNav'].options));
5099 this.attachCommandLineWidget();
5100 window.addEventListener('keydown', function(e) {
5101 // console.log(e.keyCode);
5102 modules['keyboardNav'].handleKeyPress(e);
5104 this.scanPageForKeyboardLinks();
5105 // listen for new DOM nodes so that modules like autopager, never ending reddit, "load more comments" etc still get keyboard nav.
5106 if (RESUtils.pageType() === 'comments') {
5107 RESUtils.watchForElement('newComments', modules['keyboardNav'].scanPageForNewKeyboardLinks);
5109 RESUtils.watchForElement('siteTable', modules['keyboardNav'].scanPageForNewKeyboardLinks);
5113 scanPageForNewKeyboardLinks: function() {
5114 modules['keyboardNav'].scanPageForKeyboardLinks(true);
5116 setKeyIndex: function() {
5117 var trimLoc = location.href;
5118 // remove any trailing slash from the URL
5119 if (trimLoc.substr(-1) === '/') trimLoc = trimLoc.substr(0,trimLoc.length-1);
5120 if (typeof this.keyboardNavLastIndexCache[trimLoc] === 'undefined') {
5121 this.keyboardNavLastIndexCache[trimLoc] = {};
5123 var now = new Date().getTime();
5124 this.keyboardNavLastIndexCache[trimLoc] = {
5125 index: this.activeIndex,
5128 RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
5130 handleScroll: function(e) {
5131 if (modules['keyboardNav'].scrollTimer) clearTimeout(modules['keyboardNav'].scrollTimer);
5132 modules['keyboardNav'].scrollTimer = setTimeout(modules['keyboardNav'].handleScrollAfterTimer, 300);
5134 handleScrollAfterTimer: function() {
5135 if ((! modules['keyboardNav'].recentKeyPress) && (! RESUtils.elementInViewport(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]))) {
5136 for (var i=0, len=modules['keyboardNav'].keyboardLinks.length; i<len; i++) {
5137 if (RESUtils.elementInViewport(modules['keyboardNav'].keyboardLinks[i])) {
5138 modules['keyboardNav'].keyUnfocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
5139 modules['keyboardNav'].activeIndex = i;
5140 modules['keyboardNav'].keyFocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
5146 attachCommandLineWidget: function() {
5147 this.commandLineWidget = createElementWithID('div','keyCommandLineWidget');
5148 this.commandLineInput = createElementWithID('input','keyCommandInput');
5149 this.commandLineInput.setAttribute('type','text');
5150 this.commandLineInput.addEventListener('blur', function(e) {
5151 modules['keyboardNav'].toggleCmdLine(false);
5153 this.commandLineInput.addEventListener('keyup', function(e) {
5154 if (e.keyCode === 27) {
5156 modules['keyboardNav'].toggleCmdLine(false);
5159 modules['keyboardNav'].cmdLineHelper(e.target.value);
5162 this.commandLineInputTip = createElementWithID('div','keyCommandInputTip');
5163 this.commandLineInputError = createElementWithID('div','keyCommandInputError');
5166 this.commandLineSubmit = createElementWithID('input','keyCommandInput');
5167 this.commandLineSubmit.setAttribute('type','submit');
5168 this.commandLineSubmit.setAttribute('value','go');
5170 this.commandLineForm = createElementWithID('form','keyCommandForm');
5171 this.commandLineForm.appendChild(this.commandLineInput);
5172 // this.commandLineForm.appendChild(this.commandLineSubmit);
5173 var txt = document.createTextNode('type a command, ? for help, esc to close');
5174 this.commandLineForm.appendChild(txt);
5175 this.commandLineForm.appendChild(this.commandLineInputTip);
5176 this.commandLineForm.appendChild(this.commandLineInputError);
5177 this.commandLineForm.addEventListener('submit', modules['keyboardNav'].cmdLineSubmit, false);
5178 this.commandLineWidget.appendChild(this.commandLineForm);
5179 document.body.appendChild(this.commandLineWidget);
5182 cmdLineHelper: function (val) {
5183 var splitWords = val.split(' '),
5184 command = splitWords[0],
5186 splitWords.splice(0,1);
5187 val = splitWords.join(' ');
5188 if (command.slice(0,2) === 'r/') {
5189 // get the subreddit name they've typed so far (anything after r/)...
5190 srString = command.slice(2);
5191 this.cmdLineShowTip('navigate to subreddit: ' + srString);
5192 } else if (command.match('/?u/\\w+/m/')) {
5193 str = 'navigate to multi-reddit: ';
5194 str += (command.indexOf('/') > 0 ? '/' : '') + command;
5195 this.cmdLineShowTip(str);
5196 } else if (command.slice(0,2) === 'm/') {
5197 str = 'navigate to multi-reddit: /me/' + command;
5198 this.cmdLineShowTip(str);
5199 } else if (command.slice(0,2) === 'u/') {
5200 // get the user name they've typed so far (anything after u/)...
5201 var userString = command.slice(2);
5202 this.cmdLineShowTip('navigate to user profile: ' + userString);
5203 } else if (command.slice(0,1) === '/') {
5204 srString = command.slice(1);
5205 this.cmdLineShowTip('sort by ([n]ew, [t]op, [h]ot, [c]ontroversial): ' + srString);
5206 } else if (command === 'tag') {
5207 if ((typeof this.cmdLineTagUsername === 'undefined') || (this.cmdLineTagUsername === '')) {
5208 var searchArea = modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex];
5209 var authorLink = searchArea.querySelector('a.author');
5210 this.cmdLineTagUsername = authorLink.innerHTML;
5212 str = 'tag user ' + this.cmdLineTagUsername;
5214 str += ' as: ' + val;
5216 this.cmdLineShowTip(str);
5217 } else if (command === 'user') {
5218 str = 'go to profile';
5220 str += ' for: ' + val;
5222 this.cmdLineShowTip(str);
5223 } else if (command === 'sw') {
5224 this.cmdLineShowTip('Switch users to: ' + val);
5225 } else if (command === 'm') {
5226 this.cmdLineShowTip('View messages.');
5227 } else if (command === 'mm') {
5228 this.cmdLineShowTip('View moderator mail.');
5229 } else if (command === 'ls') {
5230 this.cmdLineShowTip('Toggle lightSwitch.');
5231 } else if (command === 'nsfw') {
5232 this.cmdLineShowTip('Toggle nsfw filter on or off');
5233 } else if (command === 'srstyle') {
5234 str = 'toggle subreddit style';
5236 str += ' for: ' + val;
5238 if (RESUtils.currentSubreddit()) {
5239 str += ' for: ' + RESUtils.currentSubreddit();
5242 this.cmdLineShowTip(str);
5243 } else if (command === 'search') {
5244 this.cmdLineShowTip('Search RES settings for: ' + val);
5245 } else if (command === 'XHRCache') {
5246 this.cmdLineShowTip('clear - clear the cache (use if inline images aren\'t loading properly)');
5247 } else if (command.slice(0,1) === '?') {
5248 str = 'Currently supported commands:';
5250 str += '<li>r/[subreddit] - navigates to subreddit</li>';
5251 str += '<li>/n, /t, /h or /c - goes to new, top, hot or controversial sort of current subreddit</li>';
5252 str += '<li>[number] - navigates to the link with that number (comments pages) or rank (link pages)</li>';
5253 str += '<li>tag [text] - tags author of currently selected link/comment as text</li>';
5254 str += '<li>sw [username] - switch users to [username]</li>';
5255 str += '<li>user [username] or u/[username] - view profile for [username]</li>';
5256 str += '<li>u/[username]/m/[multi] - view the multireddit [multi] curated by [username]</li>';
5257 str += '<li>m/[multi] - view your multireddit [multi]';
5258 str += '<li>m - go to inbox</li>';
5259 str += '<li>mm - go to moderator mail</li>';
5260 str += '<li>ls - toggle lightSwitch</li>';
5261 str += '<li>nsfw [on|off] - toggle nsfw filter on/off</li>';
5262 str += '<li>srstyle [subreddit] [on|off] - toggle subreddit style on/off (if no subreddit is specified, uses current subreddit)</li>';
5263 str += '<li>search [words to search for]- search RES settings</li>';
5264 str += '<li>RESStorage [get|set|update|remove] [key] [value] - For debug use only, you shouldn\'t mess with this unless you know what you\'re doing.</li>';
5265 str += '<li>XHRCache clear - manipulate the XHR cache </li>';
5267 this.cmdLineShowTip(str);
5269 this.cmdLineShowTip('');
5272 cmdLineShowTip: function(str) {
5273 $(this.commandLineInputTip).html(str);
5275 cmdLineShowError: function(str) {
5276 $(this.commandLineInputError).html(str);
5278 toggleCmdLine: function(force) {
5279 var open = ((force == null || force) && (this.commandLineWidget.style.display !== 'block'));
5280 delete this.cmdLineTagUsername;
5282 this.cmdLineShowError('');
5283 this.commandLineWidget.style.display = 'block';
5284 setTimeout(function() {
5285 modules['keyboardNav'].commandLineInput.focus();
5287 this.commandLineInput.value = '';
5289 modules['keyboardNav'].commandLineInput.blur();
5290 this.commandLineWidget.style.display = 'none';
5292 modules['styleTweaks'].setSRStyleToggleVisibility(!open, 'cmdline');
5294 cmdLineSubmit: function(e) {
5296 $(modules['keyboardNav'].commandLineInputError).html('');
5297 var theInput = modules['keyboardNav'].commandLineInput.value;
5298 // see what kind of input it is:
5299 if (theInput.match('^\/?r/')) {
5300 // subreddit? (r/subreddit or /r/subreddit)
5301 theInput = theInput.replace(/^\/?r\//,'');
5302 location.href = '/r/'+theInput;
5303 } else if (theInput.match('^\/?m/')) {
5304 theInput = theInput.replace(/^\/?m\//,'');
5305 location.href = '/me/m/'+theInput;
5306 } else if (theInput.match('^\/?u/')) {
5307 // subreddit? (r/subreddit or /r/subreddit)
5308 theInput = theInput.replace(/^\/?u\//,'');
5309 location.href = '/u/'+theInput;
5310 } else if (theInput.indexOf('/') === 0) {
5312 theInput = theInput.slice(1);
5324 theInput = 'controversial';
5327 validSorts = ['new','top','hot','controversial'];
5328 if (validSorts.indexOf(theInput) !== -1) {
5329 if (RESUtils.currentUserProfile()) {
5330 location.href = '/user/'+RESUtils.currentUserProfile()+'?sort='+theInput;
5331 } else if (RESUtils.currentSubreddit()) {
5332 location.href = '/r/'+RESUtils.currentSubreddit()+'/'+theInput;
5334 location.href = '/'+theInput;
5337 modules['keyboardNav'].cmdLineShowError('invalid sort command - must be [n]ew, [t]op, [h]ot or [c]ontroversial');
5340 } else if (!(isNaN(parseInt(theInput, 10)))) {
5341 if (RESUtils.pageType() === 'comments') {
5342 // comment link number? (integer)
5343 modules['keyboardNav'].commentLink(parseInt(theInput, 10)-1);
5344 } else if (RESUtils.pageType() === 'linklist') {
5345 modules['keyboardNav'].keyUnfocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
5346 modules['keyboardNav'].activeIndex = parseInt(theInput, 10) - 1;
5347 modules['keyboardNav'].keyFocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
5348 modules['keyboardNav'].followLink();
5351 var splitWords = theInput.split(' ');
5352 var command = splitWords[0];
5353 splitWords.splice(0,1);
5354 var val = splitWords.join(' ');
5357 var searchArea = modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex];
5358 var tagLink = searchArea.querySelector('a.userTagLink');
5360 RESUtils.click(tagLink);
5361 setTimeout(function() {
5363 document.getElementById('userTaggerTag').value = val;
5369 // switch accounts (username is required)
5370 if (val.length <= 1) {
5371 modules['keyboardNav'].cmdLineShowError('No username specified.');
5374 // first make sure the account exists...
5375 var accounts = modules['accountSwitcher'].options.accounts.value;
5377 for (var i=0, len=accounts.length; i<len; i++) {
5378 var thisPair = accounts[i];
5379 if (thisPair[0] == val) {
5384 modules['accountSwitcher'].switchTo(val);
5386 modules['keyboardNav'].cmdLineShowError('No such username in accountSwitcher.');
5392 // view profile for username (username is required)
5393 if (val.length <= 1) {
5394 modules['keyboardNav'].cmdLineShowError('No username specified.');
5397 location.href = '/user/' + val;
5401 // view JSON data for username (username is required)
5402 if (val.length <= 1) {
5403 modules['keyboardNav'].cmdLineShowError('No username specified.');
5408 url: location.protocol + "//"+location.hostname+"/user/" + val + "/about.json?app=res",
5409 onload: function(response) {
5410 alert(response.responseText);
5416 // get CSS code for a badge for username (username is required)
5417 if (val.length <= 1) {
5418 modules['keyboardNav'].cmdLineShowError('No username specified.');
5423 url: location.protocol + "//"+location.hostname+"/user/" + val + "/about.json?app=res",
5424 onload: function(response) {
5425 var thisResponse = JSON.parse(response.responseText);
5426 var css = ', .id-t2_'+thisResponse.data.id+':before';
5434 location.href = '/message/inbox/';
5438 location.href = '/message/moderator/';
5441 // toggle lightSwitch
5442 RESUtils.click(modules['styleTweaks'].lightSwitch);
5446 switch (val && val.toLowerCase()) {
5454 modules['filteReddit'].toggleNsfwFilter(toggle, true);
5457 // toggle subreddit style
5460 splitWords = val.split(' ');
5461 if (splitWords.length === 2) {
5463 toggleText = splitWords[1];
5465 sr = RESUtils.currentSubreddit();
5466 toggleText = splitWords[0];
5469 modules['keyboardNav'].cmdLineShowError('No subreddit specified.');
5472 if (toggleText === 'on') {
5474 } else if (toggleText === 'off') {
5477 modules['keyboardNav'].cmdLineShowError('You must specify "on" or "off".');
5480 var action = (toggle) ? 'enabled' : 'disabled';
5481 modules['styleTweaks'].toggleSubredditStyle(toggle, sr);
5482 RESUtils.notification({
5483 header: 'Subreddit Style',
5484 moduleID: 'styleTweaks',
5485 message: 'Subreddit style '+action+' for subreddit: '+sr
5488 case 'notification':
5489 // test notification
5490 RESUtils.notification(val, 4000);
5493 modules['settingsNavigation'].search(val);
5496 // get or set RESStorage data
5497 splitWords = val.split(' ');
5498 if (splitWords.length < 2) {
5499 modules['keyboardNav'].cmdLineShowError('You must specify "get [key]", "update [key]" or "set [key] [value]"');
5501 var command = splitWords[0];
5502 var key = splitWords[1];
5503 if (splitWords.length > 2) {
5504 splitWords.splice(0,2);
5505 var value = splitWords.join(' ');
5507 // console.log(command);
5508 if (command === 'get') {
5509 alert('Value of RESStorage['+key+']: <br><br><textarea rows="5" cols="50">' + RESStorage.getItem(key) + '</textarea>');
5510 } else if (command === 'update') {
5511 var now = new Date().getTime();
5512 alert('Value of RESStorage['+key+']: <br><br><textarea id="RESStorageUpdate'+now+'" rows="5" cols="50">' + RESStorage.getItem(key) + '</textarea>', function() {
5513 var textArea = document.getElementById('RESStorageUpdate'+now);
5515 var value = textArea.value;
5516 RESStorage.setItem(key, value);
5519 } else if (command === 'remove') {
5520 RESStorage.removeItem(key);
5521 alert('RESStorage['+key+'] deleted');
5522 } else if (command === 'set') {
5523 RESStorage.setItem(key, value);
5524 alert('RESStorage['+key+'] set to:<br><br><textarea rows="5" cols="50">' + value + '</textarea>');
5526 modules['keyboardNav'].cmdLineShowError('You must specify either "get [key]" or "set [key] [value]"');
5531 splitWords = val.split(' ');
5532 if (splitWords.length < 1) {
5533 modules['keyboardNav'].cmdLineShowError('Operation required [clear]');
5535 switch (splitWords[0]) {
5537 RESUtils.xhrCache('clear');
5540 modules['keyboardNav'].cmdLineShowError('The only accepted operation is <tt>clear</tt>');
5546 // user is already looking at help... do nothing.
5550 modules['keyboardNav'].cmdLineShowError('unknown command - type ? for help');
5555 // hide the commandline tool...
5556 modules['keyboardNav'].toggleCmdLine(false);
5558 scanPageForKeyboardLinks: function(isNew) {
5559 if (typeof isNew === 'undefined') {
5562 // check if we're on a link listing (regular page, subreddit page, etc) or comments listing...
5563 this.pageType = RESUtils.pageType();
5564 switch(this.pageType) {
5567 // get all links into an array...
5568 var siteTable = document.querySelector('#siteTable');
5569 var stMultiCheck = document.querySelectorAll('#siteTable');
5570 // 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!)
5571 if (stMultiCheck.length === 2) {
5572 siteTable = stMultiCheck[1];
5575 this.keyboardLinks = document.body.querySelectorAll('div.linklisting .entry');
5577 if ((this.keyboardNavLastIndexCache[location.href]) && (this.keyboardNavLastIndexCache[location.href].index > 0)) {
5578 this.activeIndex = this.keyboardNavLastIndexCache[location.href].index;
5580 this.activeIndex = 0;
5582 if ((this.keyboardNavLastIndexCache[location.href]) && (this.keyboardNavLastIndexCache[location.href].index >= this.keyboardLinks.length)) {
5583 this.activeIndex = 0;
5589 // get all links into an array...
5590 this.keyboardLinks = document.body.querySelectorAll('#siteTable .entry, div.content > div.commentarea .entry');
5592 this.activeIndex = 0;
5596 var siteTable = document.querySelector('#siteTable');
5598 this.keyboardLinks = siteTable.querySelectorAll('.entry');
5599 this.activeIndex = 0;
5603 // wire up keyboard links for mouse clicky selecty goodness...
5604 if ((typeof this.keyboardLinks !== 'undefined') && (this.options.clickFocus.value)) {
5605 for (var i=0, len=this.keyboardLinks.length;i<len;i++) {
5606 this.keyboardLinks[i].setAttribute('keyIndex', i);
5607 // changed parentElement to parentNode for FF3.6 compatibility.
5608 this.keyboardLinks[i].parentNode.addEventListener('click', (function(e) {
5609 var thisIndex = parseInt(this.getAttribute('keyIndex'), 10);
5610 if (modules['keyboardNav'].activeIndex != thisIndex) {
5611 modules['keyboardNav'].keyUnfocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
5612 modules['keyboardNav'].activeIndex = thisIndex;
5613 modules['keyboardNav'].keyFocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
5615 }).bind(this.keyboardLinks[i]), true);
5617 this.keyFocus(this.keyboardLinks[this.activeIndex]);
5620 recentKey: function() {
5621 modules['keyboardNav'].recentKeyPress = true;
5622 clearTimeout(modules['keyboardNav'].recentKey);
5623 modules['keyboardNav'].recentKeyTimer = setTimeout(function() {
5624 modules['keyboardNav'].recentKeyPress = false;
5627 keyFocus: function(obj) {
5628 if ((typeof obj !== 'undefined') && (obj.classList.contains('RES-keyNav-activeElement'))) {
5630 } else if (typeof obj !== 'undefined') {
5631 obj.classList.add('RES-keyNav-activeElement');
5632 this.activeElement = obj;
5633 if ((this.pageType === 'linklist') || (this.pageType === 'profile')) {
5636 if ((this.pageType === 'comments') && (this.options.commentsLinkNumbers.value)) {
5637 var links = this.getCommentLinks(obj);
5638 var annotationCount = 0;
5639 for (var i=0, len=links.length; i<len; i++) {
5640 if (!(links[i].classList.contains('madeVisible') ||
5641 links[i].classList.contains('toggleImage') ||
5642 links[i].classList.contains('noKeyNav') ||
5643 RESUtils.isCommentCode(links[i]))) {
5644 var annotation = document.createElement('span');
5646 $(annotation).text('['+annotationCount+'] ');
5647 annotation.title = 'press '+annotationCount+' to open link';
5648 annotation.classList.add('keyNavAnnotation');
5650 if (!(hasClass(links[i],'hasListener'))) {
5651 addClass(links[i],'hasListener');
5652 links[i].addEventListener('click', modules['keyboardNav'].handleKeyLink, true);
5655 if (modules['keyboardNav'].options.commentsLinkNumberPosition.value === 'right') {
5656 insertAfter(links[i], annotation);
5658 links[i].parentNode.insertBefore(annotation, links[i]);
5665 handleKeyLink: function(link) {
5667 if ((modules['keyboardNav'].options.commentsLinkNewTab.value) || e.ctrlKey) {
5670 if (link.classList.contains('toggleImage')) {
5671 RESUtils.click(link);
5674 var thisURL = link.getAttribute('href'),
5675 isLocalToPage = (thisURL.indexOf('reddit') !== -1) && (thisURL.indexOf('comments') !== -1) && (thisURL.indexOf('#') !== -1);
5676 if ((!isLocalToPage) && (button === 1)) {
5678 if (BrowserDetect.isChrome()) {
5680 requestType: 'keyboardNav',
5684 chrome.extension.sendMessage(thisJSON);
5685 } else if (BrowserDetect.isSafari()) {
5687 requestType: 'keyboardNav',
5691 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
5692 } else if (BrowserDetect.isOpera()) {
5694 requestType: 'keyboardNav',
5698 opera.extension.postMessage(JSON.stringify(thisJSON));
5699 } else if (BrowserDetect.isFirefox()) {
5701 requestType: 'keyboardNav',
5705 self.postMessage(thisJSON);
5707 window.open(this.getAttribute('href'));
5710 location.href = this.getAttribute('href');
5713 keyUnfocus: function(obj) {
5714 obj.classList.remove('RES-keyNav-activeElement');
5715 if (this.pageType === 'comments') {
5716 var annotations = obj.querySelectorAll('div.md .keyNavAnnotation');
5717 for (var i=0, len=annotations.length; i<len; i++) {
5718 annotations[i].parentNode.removeChild(annotations[i]);
5721 RESUtils.hover.close(false);
5723 drawHelp: function() {
5724 var thisHelp = createElementWithID('div','keyHelp');
5725 var helpTable = document.createElement('table');
5726 thisHelp.appendChild(helpTable);
5727 var helpTableHeader = document.createElement('thead');
5728 var helpTableHeaderRow = document.createElement('tr');
5729 var helpTableHeaderKey = document.createElement('th');
5730 $(helpTableHeaderKey).text('Key');
5731 helpTableHeaderRow.appendChild(helpTableHeaderKey);
5732 var helpTableHeaderFunction = document.createElement('th');
5733 $(helpTableHeaderFunction).text('Function');
5734 helpTableHeaderRow.appendChild(helpTableHeaderFunction);
5735 helpTableHeader.appendChild(helpTableHeaderRow);
5736 helpTable.appendChild(helpTableHeader);
5737 var helpTableBody = document.createElement('tbody');
5738 var isLink = /^link[\d]+$/i;
5739 for (var i in this.options) {
5740 if ((this.options[i].type === 'keycode') && (!isLink.test(i))) {
5741 var thisRow = document.createElement('tr');
5742 var thisRowKey = document.createElement('td');
5743 var thisKeyCode = this.getNiceKeyCode(i);
5744 $(thisRowKey).html(thisKeyCode);
5745 thisRow.appendChild(thisRowKey);
5746 var thisRowDesc = document.createElement('td');
5747 $(thisRowDesc).html(this.options[i].description);
5748 thisRow.appendChild(thisRowDesc);
5749 helpTableBody.appendChild(thisRow);
5752 helpTable.appendChild(helpTableBody);
5753 document.body.appendChild(thisHelp);
5755 getNiceKeyCode: function(optionKey) {
5756 var keyCodeArray = this.options[optionKey].value;
5757 if (!keyCodeArray) return;
5759 if (typeof keyCodeArray === 'string') {
5760 keyCodeArray = parseInt(keyCodeArray);
5762 if (typeof keyCodeArray === 'number') {
5763 keyCodeArray = [keyCodeArray, false, false, false, false];
5765 var niceKeyCode = RESUtils.niceKeyCode(keyCodeArray);
5768 handleKeyPress: function(e) {
5769 var konamitest = (typeof konami === 'undefined') || (!konami.almostThere);
5770 if ((document.activeElement.tagName === 'BODY') && (konamitest)) {
5771 // comments page, or link list?
5772 var keyArray = [e.keyCode, e.altKey, e.ctrlKey, e.shiftKey, e.metaKey];
5773 switch(this.pageType) {
5777 case checkKeysForEvent(e, this.options.moveUp.value):
5780 case checkKeysForEvent(e, this.options.moveDown.value):
5783 case checkKeysForEvent(e, this.options.moveTop.value):
5786 case checkKeysForEvent(e, this.options.moveBottom.value):
5789 case checkKeysForEvent(e, this.options.followLink.value):
5792 case checkKeysForEvent(e, this.options.followLinkNewTab.value):
5794 this.followLink(true);
5796 case checkKeysForEvent(e, this.options.followComments.value):
5797 this.followComments();
5799 case checkKeysForEvent(e, this.options.followCommentsNewTab.value):
5801 this.followComments(true);
5803 case checkKeysForEvent(e, this.options.toggleExpando.value):
5804 this.toggleExpando();
5806 case checkKeysForEvent(e, this.options.imageSizeUp.value):
5809 case checkKeysForEvent(e, this.options.imageSizeDown.value):
5810 this.imageSizeDown();
5812 case checkKeysForEvent(e, this.options.imageSizeUpFine.value):
5813 this.imageSizeUp(true);
5815 case checkKeysForEvent(e, this.options.imageSizeDownFine.value):
5816 this.imageSizeDown(true);
5818 case checkKeysForEvent(e, this.options.previousGalleryImage.value):
5819 this.previousGalleryImage();
5821 case checkKeysForEvent(e, this.options.nextGalleryImage.value):
5822 this.nextGalleryImage();
5824 case checkKeysForEvent(e, this.options.toggleViewImages.value):
5825 this.toggleViewImages();
5827 case checkKeysForEvent(e, this.options.followLinkAndCommentsNewTab.value):
5829 this.followLinkAndComments();
5831 case checkKeysForEvent(e, this.options.followLinkAndCommentsNewTabBG.value):
5833 this.followLinkAndComments(true);
5835 case checkKeysForEvent(e, this.options.upVote.value):
5838 case checkKeysForEvent(e, this.options.downVote.value):
5839 this.downVote(true);
5841 case checkKeysForEvent(e, this.options.save.value):
5844 case checkKeysForEvent(e, this.options.inbox.value):
5848 case checkKeysForEvent(e, this.options.inboxNewTab.value):
5852 case checkKeysForEvent(e, this.options.profile.value):
5856 case checkKeysForEvent(e, this.options.profileNewTab.value):
5860 case checkKeysForEvent(e, this.options.frontPage.value):
5864 case checkKeysForEvent(e, this.options.nextPage.value):
5868 case checkKeysForEvent(e, this.options.prevPage.value):
5872 case checkKeysForEvent(e, this.options.toggleHelp.value):
5875 case checkKeysForEvent(e, this.options.toggleCmdLine.value):
5876 this.toggleCmdLine();
5878 case checkKeysForEvent(e, this.options.hide.value):
5881 case checkKeysForEvent(e, this.options.followSubreddit.value):
5882 this.followSubreddit();
5884 case checkKeysForEvent(e, this.options.followSubredditNewTab.value):
5885 this.followSubreddit(true);
5888 // do nothing. unrecognized key.
5894 case checkKeysForEvent(e, this.options.toggleHelp.value):
5897 case checkKeysForEvent(e, this.options.toggleCmdLine.value):
5898 this.toggleCmdLine();
5900 case checkKeysForEvent(e, this.options.moveUp.value):
5903 case checkKeysForEvent(e, this.options.moveDown.value):
5906 case checkKeysForEvent(e, this.options.moveUpSibling.value):
5907 this.moveUpSibling();
5909 case checkKeysForEvent(e, this.options.moveDownSibling.value):
5910 this.moveDownSibling();
5912 case checkKeysForEvent(e, this.options.moveUpThread.value):
5913 this.moveUpThread();
5915 case checkKeysForEvent(e, this.options.moveDownThread.value):
5916 this.moveDownThread();
5918 case checkKeysForEvent(e, this.options.moveToTopComment.value):
5919 this.moveToTopComment();
5921 case checkKeysForEvent(e, this.options.moveToParent.value):
5922 this.moveToParent();
5924 case checkKeysForEvent(e, this.options.showParents.value):
5927 case checkKeysForEvent(e, this.options.toggleChildren.value):
5928 this.toggleChildren();
5930 case checkKeysForEvent(e, this.options.followLinkNewTab.value):
5931 // only execute if the link is selected on a comments page...
5932 if (this.activeIndex === 0) {
5934 this.followLink(true);
5937 case checkKeysForEvent(e, this.options.save.value):
5938 if (this.activeIndex === 0) {
5944 case checkKeysForEvent(e, this.options.toggleExpando.value):
5945 this.toggleAllExpandos();
5947 case checkKeysForEvent(e, this.options.previousGalleryImage.value):
5948 this.previousGalleryImage();
5950 case checkKeysForEvent(e, this.options.imageSizeUp.value):
5953 case checkKeysForEvent(e, this.options.imageSizeDown.value):
5954 this.imageSizeDown();
5956 case checkKeysForEvent(e, this.options.imageSizeUpFine.value):
5957 this.imageSizeUp(true);
5959 case checkKeysForEvent(e, this.options.imageSizeDownFine.value):
5960 this.imageSizeDown(true);
5962 case checkKeysForEvent(e, this.options.nextGalleryImage.value):
5963 this.nextGalleryImage();
5965 case checkKeysForEvent(e, this.options.toggleViewImages.value):
5966 this.toggleViewImages();
5968 case checkKeysForEvent(e, this.options.upVote.value):
5971 case checkKeysForEvent(e, this.options.downVote.value):
5974 case checkKeysForEvent(e, this.options.reply.value):
5978 case checkKeysForEvent(e, this.options.inbox.value):
5982 case checkKeysForEvent(e, this.options.inboxNewTab.value):
5986 case checkKeysForEvent(e, this.options.profile.value):
5990 case checkKeysForEvent(e, this.options.profileNewTab.value):
5994 case checkKeysForEvent(e, this.options.frontPage.value):
5998 case checkKeysForEvent(e, this.options.subredditFrontPage.value):
6000 this.frontPage(true);
6002 case checkKeysForEvent(e, this.options.link1.value):
6004 this.commentLink(0);
6006 case checkKeysForEvent(e, this.options.link2.value):
6008 this.commentLink(1);
6010 case checkKeysForEvent(e, this.options.link3.value):
6012 this.commentLink(2);
6014 case checkKeysForEvent(e, this.options.link4.value):
6016 this.commentLink(3);
6018 case checkKeysForEvent(e, this.options.link5.value):
6020 this.commentLink(4);
6022 case checkKeysForEvent(e, this.options.link6.value):
6024 this.commentLink(5);
6026 case checkKeysForEvent(e, this.options.link7.value):
6028 this.commentLink(6);
6030 case checkKeysForEvent(e, this.options.link8.value):
6032 this.commentLink(7);
6034 case checkKeysForEvent(e, this.options.link9.value):
6036 this.commentLink(8);
6038 case checkKeysForEvent(e, this.options.link10.value):
6040 this.commentLink(9);
6043 // do nothing. unrecognized key.
6049 case checkKeysForEvent(e, this.options.toggleHelp.value):
6052 case checkKeysForEvent(e, this.options.toggleCmdLine.value):
6053 this.toggleCmdLine();
6055 case checkKeysForEvent(e, this.options.moveUp.value):
6058 case checkKeysForEvent(e, this.options.moveDown.value):
6061 case checkKeysForEvent(e, this.options.toggleChildren.value):
6062 this.toggleChildren();
6064 case checkKeysForEvent(e, this.options.upVote.value):
6067 case checkKeysForEvent(e, this.options.downVote.value):
6070 case checkKeysForEvent(e, this.options.reply.value):
6074 case checkKeysForEvent(e, this.options.frontPage.value):
6079 // do nothing. unrecognized key.
6085 // console.log('ignored keypress');
6088 toggleHelp: function() {
6089 (document.getElementById('keyHelp').style.display === 'block') ? this.hideHelp() : this.showHelp();
6091 showHelp: function() {
6093 RESUtils.fadeElementIn(document.getElementById('keyHelp'), 0.3);
6094 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'keyboardnavhelp');
6096 hideHelp: function() {
6098 RESUtils.fadeElementOut(document.getElementById('keyHelp'), 0.3);
6099 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'keyboardnavhelp');
6102 // find the hide link and click it...
6103 var hideLink = this.keyboardLinks[this.activeIndex].querySelector('form.hide-button > span > a');
6104 RESUtils.click(hideLink);
6105 // if ((this.options.onHideMoveDown.value) && (!modules['betteReddit'].options.fixHideLink.value)) {
6106 if (this.options.onHideMoveDown.value) {
6110 followSubreddit: function(newWindow) {
6111 // find the subreddit link and click it...
6112 var srLink = this.keyboardLinks[this.activeIndex].querySelector('A.subreddit');
6114 var thisHREF = srLink.getAttribute('href');
6116 var button = (this.options.followLinkNewTabFocus.value) ? 0 : 1,
6118 if (BrowserDetect.isChrome()) {
6120 requestType: 'keyboardNav',
6124 chrome.extension.sendMessage(thisJSON);
6125 } else if (BrowserDetect.isSafari()) {
6127 requestType: 'keyboardNav',
6131 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
6132 } else if (BrowserDetect.isOpera()) {
6134 requestType: 'keyboardNav',
6138 opera.extension.postMessage(JSON.stringify(thisJSON));
6139 } else if (BrowserDetect.isFirefox()) {
6141 requestType: 'keyboardNav',
6145 self.postMessage(thisJSON);
6147 window.open(thisHREF);
6150 location.href = thisHREF;
6154 moveUp: function() {
6155 if (this.activeIndex > 0) {
6156 this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
6158 var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6159 // skip over hidden elements...
6160 while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex > 0)) {
6162 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6164 this.keyFocus(this.keyboardLinks[this.activeIndex]);
6165 if ((!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) || (this.options.scrollStyle.value === 'top')) {
6166 RESUtils.scrollTo(0,thisXY.y);
6169 modules['keyboardNav'].recentKey();
6172 moveDown: function() {
6173 if (this.activeIndex < this.keyboardLinks.length-1) {
6174 this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
6176 var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6177 // skip over hidden elements...
6178 while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex < this.keyboardLinks.length-1)) {
6180 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6182 this.keyFocus(this.keyboardLinks[this.activeIndex]);
6183 // console.log('xy: ' + RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]).toSource());
6185 if ((!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) || (this.options.scrollTop.value)) {
6186 RESUtils.scrollTo(0,thisXY.y);
6189 if (this.options.scrollStyle.value === 'top') {
6190 RESUtils.scrollTo(0,thisXY.y);
6191 } else if ((!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex])))) {
6192 var thisHeight = this.keyboardLinks[this.activeIndex].offsetHeight;
6193 if (this.options.scrollStyle.value === 'page') {
6194 RESUtils.scrollTo(0,thisXY.y);
6196 RESUtils.scrollTo(0,thisXY.y - window.innerHeight + thisHeight + 5);
6199 if ((RESUtils.pageType() === 'linklist') && (this.activeIndex == (this.keyboardLinks.length-1) && (modules['neverEndingReddit'].isEnabled() && modules['neverEndingReddit'].options.autoLoad.value))) {
6202 modules['keyboardNav'].recentKey();
6205 moveTop: function() {
6206 this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
6207 this.activeIndex = 0;
6208 this.keyFocus(this.keyboardLinks[this.activeIndex]);
6209 var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6210 if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
6211 RESUtils.scrollTo(0,thisXY.y);
6213 modules['keyboardNav'].recentKey();
6215 moveBottom: function() {
6216 this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
6217 this.activeIndex = this.keyboardLinks.length-1;
6218 this.keyFocus(this.keyboardLinks[this.activeIndex]);
6219 var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6220 if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
6221 RESUtils.scrollTo(0,thisXY.y);
6223 modules['keyboardNav'].recentKey();
6225 moveDownSibling: function() {
6226 if (this.activeIndex < this.keyboardLinks.length-1) {
6227 this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
6228 var thisParent = this.keyboardLinks[this.activeIndex].parentNode;
6229 var childCount = thisParent.querySelectorAll('.entry').length;
6230 this.activeIndex += childCount;
6231 // skip over hidden elements...
6232 var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6233 while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex < this.keyboardLinks.length-1)) {
6235 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6237 if ((this.pageType === 'linklist') || (this.pageType === 'profile')) {
6240 this.keyFocus(this.keyboardLinks[this.activeIndex]);
6241 if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
6242 RESUtils.scrollTo(0,thisXY.y);
6245 modules['keyboardNav'].recentKey();
6247 moveUpSibling: function() {
6248 if (this.activeIndex < this.keyboardLinks.length-1) {
6249 this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
6250 var thisParent = this.keyboardLinks[this.activeIndex].parentNode,
6252 if (thisParent.previousSibling !== null) {
6253 childCount = thisParent.previousSibling.previousSibling.querySelectorAll('.entry').length;
6257 this.activeIndex -= childCount;
6258 // skip over hidden elements...
6259 var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6260 while ((thisXY.x === 0) && (thisXY.y === 0) && (this.activeIndex < this.keyboardLinks.length-1)) {
6262 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6264 if ((this.pageType === 'linklist') || (this.pageType === 'profile')) {
6267 this.keyFocus(this.keyboardLinks[this.activeIndex]);
6268 if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
6269 RESUtils.scrollTo(0,thisXY.y);
6272 modules['keyboardNav'].recentKey();
6274 moveUpThread: function() {
6275 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
6276 this.moveToTopComment();
6278 this.moveUpSibling();
6280 moveDownThread: function() {
6281 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
6282 this.moveToTopComment();
6284 this.moveDownSibling();
6286 moveToTopComment: function() {
6287 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
6288 var firstParent = this.keyboardLinks[this.activeIndex].parentNode;
6289 //goes up to the root of the current thread
6290 while (!firstParent.parentNode.parentNode.parentNode.classList.contains('content') && (firstParent !== null)) {
6291 this.moveToParent();
6292 firstParent = this.keyboardLinks[this.activeIndex].parentNode;
6296 moveToParent: function() {
6297 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
6298 var firstParent = this.keyboardLinks[this.activeIndex].parentNode;
6299 // check if we're at the top parent, first... if the great grandparent has a class of content, do nothing.
6300 if (!firstParent.parentNode.parentNode.parentNode.classList.contains('content')) {
6301 if (firstParent !== null) {
6302 this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
6303 var thisParent = firstParent.parentNode.parentNode.previousSibling;
6304 var newKeyIndex = parseInt(thisParent.getAttribute('keyindex'), 10);
6305 this.activeIndex = newKeyIndex;
6306 this.keyFocus(this.keyboardLinks[this.activeIndex]);
6307 var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6308 if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
6309 RESUtils.scrollTo(0,thisXY.y);
6314 modules['keyboardNav'].recentKey();
6316 showParents: function() {
6317 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
6318 var firstParent = this.keyboardLinks[this.activeIndex].parentNode;
6319 if (firstParent != null) {
6320 var button = $(this.keyboardLinks[this.activeIndex]).find('.buttons :not(:first-child) .bylink:first').get(0);
6321 RESUtils.hover.begin(button, {}, modules['showParent'].showCommentHover, {});
6325 toggleChildren: function() {
6326 if (this.activeIndex === 0) {
6327 // Ahh, we're not in a comment, but in the main story... that key should follow the link.
6330 // find out if this is a collapsed or uncollapsed view...
6331 var thisCollapsed = this.keyboardLinks[this.activeIndex].querySelector('div.collapsed');
6332 var thisNonCollapsed = this.keyboardLinks[this.activeIndex].querySelector('div.noncollapsed');
6333 if (thisCollapsed.style.display !== 'none') {
6334 thisToggle = thisCollapsed.querySelector('a.expand');
6336 // check if this is a "show more comments" box, or just contracted content...
6337 moreComments = thisNonCollapsed.querySelector('span.morecomments > a');
6339 thisToggle = moreComments;
6341 thisToggle = thisNonCollapsed.querySelector('a.expand');
6343 // 'continue this thread' links
6344 contThread = thisNonCollapsed.querySelector('span.deepthread > a');
6346 thisToggle = contThread;
6349 RESUtils.click(thisToggle);
6352 toggleExpando: function() {
6353 var thisExpando = this.keyboardLinks[this.activeIndex].querySelector('.expando-button');
6355 RESUtils.click(thisExpando);
6356 if (this.options.scrollOnExpando.value) {
6357 var thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6358 RESUtils.scrollTo(0,thisXY.y);
6362 imageResize: function(factor) {
6363 var images = $(this.activeElement).find('.RESImage.loaded'),
6366 for (var i=0, len=images.length; i<len; i++) {
6367 thisWidth = images[i].width;
6368 modules['showImages'].resizeImage(images[i], thisWidth + factor);
6371 imageSizeUp: function(fineControl) {
6372 var factor = (fineControl) ? 50 : 150;
6373 this.imageResize(factor);
6375 imageSizeDown: function(fineControl) {
6376 var factor = (fineControl) ? -50 : -150;
6377 this.imageResize(factor);
6379 previousGalleryImage: function() {
6380 var previousButton = this.keyboardLinks[this.activeIndex].querySelector('.RESGalleryControls .previous');
6381 if (previousButton) {
6382 RESUtils.click(previousButton);
6385 nextGalleryImage: function() {
6386 var nextButton = this.keyboardLinks[this.activeIndex].querySelector('.RESGalleryControls .next');
6388 RESUtils.click(nextButton);
6391 toggleViewImages: function() {
6392 var thisViewImages = document.body.querySelector('#viewImagesButton');
6393 if (thisViewImages) {
6394 RESUtils.click(thisViewImages);
6397 toggleAllExpandos: function() {
6398 var thisExpandos = this.keyboardLinks[this.activeIndex].querySelectorAll('.expando-button');
6400 for (var i=0,len=thisExpandos.length; i<len; i++) {
6401 RESUtils.click(thisExpandos[i]);
6405 followLink: function(newWindow) {
6406 var thisA = this.keyboardLinks[this.activeIndex].querySelector('a.title');
6407 var thisHREF = thisA.getAttribute('href');
6408 // console.log(thisA);
6410 var button = (this.options.followLinkNewTabFocus.value) ? 0 : 1,
6412 if (BrowserDetect.isChrome()) {
6414 requestType: 'keyboardNav',
6418 chrome.extension.sendMessage(thisJSON);
6419 } else if (BrowserDetect.isSafari()) {
6421 requestType: 'keyboardNav',
6425 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
6426 } else if (BrowserDetect.isOpera()) {
6428 requestType: 'keyboardNav',
6432 opera.extension.postMessage(JSON.stringify(thisJSON));
6433 } else if (BrowserDetect.isFirefox()) {
6435 requestType: 'keyboardNav',
6439 self.postMessage(thisJSON);
6441 window.open(thisHREF);
6444 location.href = thisHREF;
6447 followComments: function(newWindow) {
6448 var thisA = this.keyboardLinks[this.activeIndex].querySelector('a.comments'),
6449 thisHREF = thisA.getAttribute('href');
6452 if (BrowserDetect.isChrome()) {
6454 requestType: 'keyboardNav',
6457 chrome.extension.sendMessage(thisJSON);
6458 } else if (BrowserDetect.isSafari()) {
6460 requestType: 'keyboardNav',
6463 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
6464 } else if (BrowserDetect.isOpera()) {
6466 requestType: 'keyboardNav',
6469 opera.extension.postMessage(JSON.stringify(thisJSON));
6470 } else if (BrowserDetect.isFirefox()) {
6472 requestType: 'keyboardNav',
6475 self.postMessage(thisJSON);
6477 window.open(thisHREF);
6480 location.href = thisHREF;
6483 followLinkAndComments: function(background) {
6484 // find the [l+c] link and click it...
6485 var lcLink = this.keyboardLinks[this.activeIndex].querySelector('.redditSingleClick');
6486 RESUtils.mousedown(lcLink, background);
6488 upVote: function(link) {
6489 if (typeof this.keyboardLinks[this.activeIndex] === 'undefined') return false;
6492 if (this.keyboardLinks[this.activeIndex].previousSibling.tagName === 'A') {
6493 upVoteButton = this.keyboardLinks[this.activeIndex].previousSibling.previousSibling.querySelector('div.up') || this.keyboardLinks[this.activeIndex].previousSibling.previousSibling.querySelector('div.upmod');
6495 upVoteButton = this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.up') || this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.upmod');
6498 RESUtils.click(upVoteButton);
6500 if (link && this.options.onVoteMoveDown.value) {
6504 downVote: function(link) {
6505 if (typeof this.keyboardLinks[this.activeIndex] === 'undefined') return false;
6508 if (this.keyboardLinks[this.activeIndex].previousSibling.tagName === 'A') {
6509 downVoteButton = this.keyboardLinks[this.activeIndex].previousSibling.previousSibling.querySelector('div.down') || this.keyboardLinks[this.activeIndex].previousSibling.previousSibling.querySelector('div.downmod');
6511 downVoteButton = this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.down') || this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.downmod');
6514 RESUtils.click(downVoteButton);
6516 if (link && this.options.onVoteMoveDown.value) {
6520 saveLink: function() {
6521 var saveLink = this.keyboardLinks[this.activeIndex].querySelector('form.save-button > span > a');
6522 if (saveLink) RESUtils.click(saveLink);
6524 saveComment: function() {
6525 var saveComment = this.keyboardLinks[this.activeIndex].querySelector('.saveComments');
6526 if (saveComment) RESUtils.click(saveComment);
6529 // activeIndex = 0 means we're at the original post, not a comment
6530 if ((this.activeIndex > 0) || (RESUtils.pageType() !== 'comments')) {
6531 if ((RESUtils.pageType() === 'comments') && (this.activeIndex === 0) && (! location.href.match('/message/'))) {
6532 $('.usertext-edit textarea:first').focus();
6534 var commentButtons = this.keyboardLinks[this.activeIndex].querySelectorAll('ul.buttons > li > a');
6535 for (var i=0, len=commentButtons.length;i<len;i++) {
6536 if (commentButtons[i].innerHTML === 'reply') {
6537 RESUtils.click(commentButtons[i]);
6542 infoBar = document.body.querySelector('.infobar');
6543 // We're on the original post, so shift keyboard focus to the comment reply box.
6545 // uh oh, we must be in a subpage, there is no first comment box. The user probably wants to reply to the OP. Let's take them to the comments page.
6546 var commentButton = this.keyboardLinks[this.activeIndex].querySelector('ul.buttons > li > a.comments');
6547 location.href = commentButton.getAttribute('href');
6549 var firstCommentBox = document.querySelector('.commentarea textarea[name=text]');
6550 firstCommentBox.focus();
6554 navigateTo: function(newWindow,thisHREF) {
6557 if (BrowserDetect.isChrome()) {
6559 requestType: 'keyboardNav',
6562 chrome.extension.sendMessage(thisJSON);
6563 } else if (BrowserDetect.isSafari()) {
6565 requestType: 'keyboardNav',
6568 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
6569 } else if (BrowserDetect.isOpera()) {
6571 requestType: 'keyboardNav',
6574 opera.extension.postMessage(JSON.stringify(thisJSON));
6576 window.open(thisHREF);
6579 location.href = thisHREF;
6582 inbox: function(newWindow) {
6583 var thisHREF = location.protocol + '//'+location.hostname+'/message/inbox/';
6584 modules['keyboardNav'].navigateTo(newWindow,thisHREF);
6586 profile: function(newWindow) {
6587 var thisHREF = location.protocol + '//'+location.hostname+'/user/'+RESUtils.loggedInUser();
6588 modules['keyboardNav'].navigateTo(newWindow,thisHREF);
6590 frontPage: function(subreddit) {
6591 var newhref = location.protocol + '//'+location.hostname+'/';
6593 newhref += 'r/' + RESUtils.currentSubreddit();
6595 location.href = newhref;
6597 nextPage: function() {
6598 // if Never Ending Reddit is enabled, just scroll to the bottom. Otherwise, click the 'next' link.
6599 if ((modules['neverEndingReddit'].isEnabled()) && (modules['neverEndingReddit'].progressIndicator)) {
6600 RESUtils.click(modules['neverEndingReddit'].progressIndicator);
6603 // get the first link to the next page of reddit...
6604 var nextPrevLinks = document.body.querySelectorAll('.content .nextprev a');
6605 if (nextPrevLinks.length > 0) {
6606 var nextLink = nextPrevLinks[nextPrevLinks.length-1];
6607 // RESUtils.click(nextLink);
6608 location.href = nextLink.getAttribute('href');
6612 prevPage: function() {
6613 // if Never Ending Reddit is enabled, do nothing. Otherwise, click the 'prev' link.
6614 if (modules['neverEndingReddit'].isEnabled()) {
6617 // get the first link to the next page of reddit...
6618 var nextPrevLinks = document.body.querySelectorAll('.content .nextprev a');
6619 if (nextPrevLinks.length > 0) {
6620 var prevLink = nextPrevLinks[0];
6621 // RESUtils.click(prevLink);
6622 location.href = prevLink.getAttribute('href');
6626 getCommentLinks: function(obj) {
6627 if (!obj) obj = this.keyboardLinks[this.activeIndex];
6628 return obj.querySelectorAll('div.md a:not(.expando-button):not(.madeVisible):not([href^="javascript:"])');
6630 commentLink: function(num) {
6631 if (this.options.commentsLinkNumbers.value) {
6632 var links = this.getCommentLinks();
6633 if (typeof links[num] !== 'undefined') {
6634 var thisLink = links[num];
6635 if ((thisLink.nextSibling) && (typeof thisLink.nextSibling.tagName !== 'undefined') && (thisLink.nextSibling.classList.contains('expando-button'))) {
6636 thisLink = thisLink.nextSibling;
6638 // RESUtils.click(thisLink);
6639 this.handleKeyLink(thisLink);
6645 // user tagger functions
6646 modules['userTagger'] = {
6647 moduleID: 'userTagger',
6648 moduleName: 'User Tagger',
6655 description: 'clickable mark for users with no tag'
6661 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).'
6666 description: 'Color users based on cumulative upvotes / downvotes'
6671 description: 'By default, store a link to the link/comment you tagged a user on'
6676 description: 'Show information on user (karma, how long they\'ve been a redditor) on hover.'
6681 description: 'Delay, in milliseconds, before hover tooltip loads. Default is 800.'
6686 description: 'Delay, in milliseconds, before hover tooltip fades away. Default is 200.'
6691 description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
6696 description: 'When clicking the "give gold" button on the user hover info on a comment, give gold to the comment.'
6701 description: 'Show "highlight" button in user hover info, for distinguishing posts/comments from particular users.'
6706 description: 'Color used to highlight a selected user, when "highlighted" from hover info.'
6708 highlightColorHover: {
6711 description: 'Color used to highlight a selected user on hover.'
6716 description: 'Show date (redditor since...) in US format (i.e. 08-31-2010)'
6721 description: 'Show the number (i.e. [+6]) rather than [vw]'
6726 description: 'Show the vote weight tooltip on hover (i.e. "your votes for...")'
6729 description: 'Adds a great deal of customization around users - tagging them, ignoring them, and more.',
6730 isEnabled: function() {
6731 return RESConsole.getModulePrefs(this.moduleID);
6733 isMatchURL: function() {
6734 return RESUtils.isMatchURL(this.moduleID);
6737 /^https?:\/\/([-\w\.]+\.)?reddit\.com\/[-\w\.]*/i
6739 beforeLoad: function() {
6740 if ((this.isEnabled()) && (this.isMatchURL())) {
6741 var css = '.comment .tagline { display: inline; }';
6742 css += '#userTaggerToolTip { display: none; position: absolute; width: 334px; height: 248px; }';
6743 css += '#userTaggerToolTip label { margin-top: 5px; clear: both; float: left; width: 110px; }';
6744 css += '#userTaggerToolTip input[type=text], #userTaggerToolTip select { margin-top: 5px; float: left; width: 195px; border: 1px solid #c7c7c7; border-radius: 3px; margin-bottom: 6px; }';
6745 css += '#userTaggerToolTip input[type=checkbox] { margin-top: 5px; float: left; }';
6746 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; } ';
6747 css += '#userTaggerToolTip .toggleButton { margin-top: 5px; margin-bottom: 5px; }';
6748 css += '#userTaggerClose { position: absolute; right: 7px; top: 7px; z-index: 11; }';
6750 css += '.ignoredUserComment { color: #CACACA; padding: 3px; font-size: 10px; }';
6751 css += '.ignoredUserPost { color: #CACACA; padding: 3px; font-size: 10px; }';
6752 css += 'a.voteWeight { text-decoration: none; color: #369; }';
6753 css += 'a.voteWeight:hover { text-decoration: none; }';
6754 css += '#authorInfoToolTip { display: none; position: absolute; min-width: 450px; z-index: 10001; }';
6755 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; }'
6756 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; }'
6757 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; }'
6758 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; }'
6759 css += '#authorInfoToolTip .authorFieldPair { clear: both; overflow: auto; margin-bottom: 12px; }';
6760 css += '#authorInfoToolTip .authorLabel { float: left; width: 140px; }';
6761 css += '#authorInfoToolTip .authorDetail { float: left; min-width: 240px; }';
6762 css += '#authorInfoToolTip .blueButton { float: right; margin-left: 8px; margin-top: 12px; }';
6763 css += '#authorInfoToolTip .redButton { float: right; margin-left: 8px; }';
6765 css += '#benefits { width: 200px; margin-left: 0; }';
6766 css += '#userTaggerToolTip #userTaggerVoteWeight { width: 30px; }';
6767 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; }';
6768 css += '.userTagLink { display: inline-block; }';
6769 css += '.hoverHelp { margin-left: 3px; cursor: pointer; color: #369; text-decoration: underline; }';
6770 css += '.userTagLink.hasTag, #userTaggerPreview { display: inline-block; padding: 0 4px; border: 1px solid #c7c7c7; border-radius: 3px; }';
6771 css += '#userTaggerPreview { float: left; height: 16px; margin-bottom: 10px; }';
6772 css += '#userTaggerToolTip .toggleButton .toggleOn { background-color: #107ac4; color: #fff; }';
6773 css += '#userTaggerToolTip .toggleButton.enabled .toggleOn { background-color: #ddd ; color: #636363; }';
6774 css += '#userTaggerToolTip .toggleButton.enabled .toggleOff { background-color: #d02020; color: #fff; }';
6775 css += '#userTaggerToolTip .toggleButton .toggleOff { background-color: #ddd; color: #636363; } ';
6776 css += '#userTaggerTable th { -moz-user-select: none; -webkit-user-select: none; -o-user-select: none; user-select: none; }'
6777 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=)}';
6779 RESUtils.addCSS(css);
6783 if ((this.isEnabled()) && (this.isMatchURL())) {
6785 this.usernameRE = /(?:u|user)\/([\w\-]+)/;
6786 // Get user tag data...
6787 var tags = RESStorage.getItem('RESmodules.userTagger.tags');
6789 if (typeof tags !== 'undefined') this.tags = safeJSON.parse(tags, 'RESmodules.userTagger.tags', true);
6790 // check if we're using the old method of storing user tags... yuck!
6791 if (this.tags === null) {
6792 this.updateTagStorage();
6794 // If we're on the dashboard, add a tab to it...
6795 if (RESUtils.currentSubreddit('dashboard')) {
6796 // add tab to dashboard
6797 modules['dashboard'].addTab('userTaggerContents','My User Tags');
6798 // populate the contents of the tab
6799 var showDiv = $('<div class="show">Show:</div>')
6800 var tagFilter = $('<select id="tagFilter"><option>tagged users</option><option>all users</option></select>')
6801 $(showDiv).append(tagFilter);
6802 $('#userTaggerContents').append(showDiv);
6803 $('#tagFilter').change(function(){
6804 modules['userTagger'].drawUserTagTable();
6807 var tagsPerPage = parseInt(modules['dashboard'].options['tagsPerPage'].value, 10);
6809 var controlWrapper = document.createElement('div');
6810 controlWrapper.id = 'tagPageControls';
6811 controlWrapper.className = 'RESGalleryControls';
6812 controlWrapper.page = 1;
6813 controlWrapper.pageCount = 1;
6815 var leftButton = document.createElement("a");
6816 leftButton.className = 'previous noKeyNav';
6817 leftButton.addEventListener('click', function(e){
6818 if (controlWrapper.page === 1) {
6819 controlWrapper.page = controlWrapper.pageCount;
6821 controlWrapper.page -= 1;
6823 modules['userTagger'].drawUserTagTable();
6825 controlWrapper.appendChild(leftButton);
6827 var posLabel = document.createElement('span');
6828 posLabel.className = 'RESGalleryLabel';
6829 posLabel.textContent = "1 of 2";
6830 controlWrapper.appendChild(posLabel);
6832 var rightButton = document.createElement("a");
6833 rightButton.className = 'next noKeyNav';
6834 rightButton.addEventListener('click', function(e){
6835 if (controlWrapper.page === controlWrapper.pageCount) {
6836 controlWrapper.page = 1;
6838 controlWrapper.page += 1;
6840 modules['userTagger'].drawUserTagTable();
6842 controlWrapper.appendChild(rightButton);
6844 $('#userTaggerContents').append(controlWrapper);
6847 var thisTable = $('<table id="userTaggerTable" />');
6848 $(thisTable).append('<thead><tr><th sort="" class="active">Username <span class="sortAsc"></span></th><th sort="tag">Tag</th><th sort="ignore">Ignored</th><th sort="color">Color</th><th sort="votes">Vote Weight</th></tr></thead><tbody></tbody>');
6849 $('#userTaggerContents').append(thisTable);
6850 $('#userTaggerTable thead th').click(function(e) {
6852 if ($(this).hasClass('delete')) {
6855 if ($(this).hasClass('active')) {
6856 $(this).toggleClass('descending');
6858 $(this).addClass('active');
6859 $(this).siblings().removeClass('active').find('SPAN').remove();
6860 $(this).find('.sortAsc, .sortDesc').remove();
6861 ($(e.target).hasClass('descending')) ? $(this).append('<span class="sortDesc" />') : $(this).append('<span class="sortAsc" />');
6862 modules['userTagger'].drawUserTagTable($(e.target).attr('sort'), $(e.target).hasClass('descending'));
6864 this.drawUserTagTable();
6869 // set up an array to cache user data
6870 this.authorInfoCache = [];
6871 if (this.options.colorUser.value) {
6872 this.attachVoteHandlers(document.body);
6874 // add tooltip to document body...
6875 this.userTaggerToolTip = createElementWithID('div','userTaggerToolTip', 'RESDialogSmall');
6876 var thisHTML = '<h3>Tag User</h3><div id="userTaggerToolTipContents" class="RESDialogContents clear">';
6877 thisHTML += '<form name="userTaggerForm" action=""><input type="hidden" id="userTaggerName" value="">';
6878 thisHTML += '<label for="userTaggerTag">Tag</label> <input type="text" id="userTaggerTag" value="">';
6879 thisHTML += '<div id="userTaggerClose" class="RESCloseButton">×</div>';
6880 thisHTML += '<label for="userTaggerColor">Color</label> <select id="userTaggerColor">';
6881 for (var color in this.bgToTextColorMap) {
6882 var bgColor = (color === 'none') ? 'transparent' : color;
6883 thisHTML += '<option style="background-color: '+bgColor+'; color: '+this.bgToTextColorMap[color]+' !important;" value="'+color+'">'+color+'</option>';
6885 thisHTML += '</select>';
6886 thisHTML += '<label for="userTaggerPreview">Preview</label> <span id="userTaggerPreview"></span>';
6887 thisHTML += '<label for="userTaggerIgnore">Ignore</label>';// <input type="checkbox" id="userTaggerIgnore" value="true">';
6888 thisHTML += '<label for="userTaggerLink">Link<span class="hoverHelp" title="add a link for this user (shows up in hover pane)">?</span></label> <input type="text" id="userTaggerLink" value="">';
6889 thisHTML += '<label for="userTaggerVoteWeight">Vote Weight<span class="hoverHelp" title="manually edit vote weight for this user">?</span></label> <input type="text" size="2" id="userTaggerVoteWeight" value="">';
6890 thisHTML += '<div class="clear"></div><input type="submit" id="userTaggerSave" value="Save"></form></div>';
6891 $(this.userTaggerToolTip).html(thisHTML);
6892 var ignoreLabel = this.userTaggerToolTip.querySelector('label[for=userTaggerIgnore]');
6893 insertAfter(ignoreLabel, RESUtils.toggleButton('userTaggerIgnore', false, 'no', 'yes'));
6894 this.userTaggerTag = this.userTaggerToolTip.querySelector('#userTaggerTag');
6895 this.userTaggerTag.addEventListener('keyup', modules['userTagger'].updateTagPreview, false);
6896 this.userTaggerColor = this.userTaggerToolTip.querySelector('#userTaggerColor');
6897 this.userTaggerColor.addEventListener('change', modules['userTagger'].updateTagPreview, false);
6898 this.userTaggerPreview = this.userTaggerToolTip.querySelector('#userTaggerPreview');
6899 var userTaggerSave = this.userTaggerToolTip.querySelector('#userTaggerSave');
6900 userTaggerSave.setAttribute('type','submit');
6901 userTaggerSave.setAttribute('value','✓ save tag');
6902 userTaggerSave.addEventListener('click', function(e) {
6904 modules['userTagger'].saveTagForm();
6906 var userTaggerClose = this.userTaggerToolTip.querySelector('#userTaggerClose');
6907 userTaggerClose.addEventListener('click', function(e) {
6908 modules['userTagger'].closeUserTagPrompt();
6910 //this.userTaggerToolTip.appendChild(userTaggerSave);
6911 this.userTaggerForm = this.userTaggerToolTip.querySelector('FORM');
6912 this.userTaggerForm.addEventListener('submit', function(e) {
6914 modules['userTagger'].saveTagForm();
6916 document.body.appendChild(this.userTaggerToolTip);
6917 if (this.options.hoverInfo.value) {
6918 this.authorInfoToolTip = createElementWithID('div', 'authorInfoToolTip', 'RESDialogSmall');
6919 this.authorInfoToolTipHeader = document.createElement('h3');
6920 this.authorInfoToolTip.appendChild(this.authorInfoToolTipHeader);
6921 this.authorInfoToolTipCloseButton = createElementWithID('div', 'authorInfoToolTipClose', 'RESCloseButton');
6922 $(this.authorInfoToolTipCloseButton).text('×');
6923 this.authorInfoToolTip.appendChild(this.authorInfoToolTipCloseButton);
6924 this.authorInfoToolTipCloseButton.addEventListener('click', function(e) {
6925 if (typeof modules['userTagger'].hideTimer !== 'undefined') {
6926 clearTimeout(modules['userTagger'].hideTimer);
6928 modules['userTagger'].hideAuthorInfo();
6930 this.authorInfoToolTipContents = createElementWithID('div','authorInfoToolTipContents', 'RESDialogContents');
6931 this.authorInfoToolTip.appendChild(this.authorInfoToolTipContents);
6932 this.authorInfoToolTip.addEventListener('mouseover', function(e) {
6933 if (typeof modules['userTagger'].hideTimer !== 'undefined') {
6934 clearTimeout(modules['userTagger'].hideTimer);
6937 this.authorInfoToolTip.addEventListener('mouseout', function(e) {
6938 if (e.target.getAttribute('class') !== 'hoverAuthor') {
6939 modules['userTagger'].hideTimer = setTimeout(function() {
6940 modules['userTagger'].hideAuthorInfo();
6941 }, modules['userTagger'].options.fadeDelay.value);
6944 document.body.appendChild(this.authorInfoToolTip);
6946 document.getElementById('userTaggerTag').addEventListener('keydown', function(e) {
6947 if (e.keyCode === 27) {
6949 modules['userTagger'].closeUserTagPrompt();
6952 //console.log('before applytags: ' + Date());
6954 //console.log('after applytags: ' + Date());
6955 if (RESUtils.pageType() === 'comments') {
6956 RESUtils.watchForElement('newComments', modules['userTagger'].attachVoteHandlers);
6957 RESUtils.watchForElement('newComments', modules['userTagger'].applyTags);
6959 RESUtils.watchForElement('siteTable', modules['userTagger'].attachVoteHandlers);
6960 RESUtils.watchForElement('siteTable', modules['userTagger'].applyTags);
6963 var userpagere = /^https?:\/\/([a-z]+).reddit.com\/user\/[-\w\.]+\/?/i;
6964 if (userpagere.test(location.href)) {
6965 var friendButton = document.querySelector('.titlebox .fancy-toggle-button');
6966 if ((typeof friendButton !== 'undefined') && (friendButton !== null)) {
6967 var firstAuthor = document.querySelector('a.author');
6968 if ((typeof firstAuthor !== 'undefined') && (firstAuthor !== null)) {
6969 var thisFriendComment = firstAuthor.getAttribute('title');
6970 thisFriendComment = (thisFriendComment !== null) ? thisFriendComment.substring(8,thisFriendComment.length-1) : '';
6972 var thisFriendComment = '';
6974 // this stopped working. commenting it out for now. if i add this back I need to check if you're reddit gold anyway.
6976 var benefitsForm = document.createElement('div');
6977 var thisUser = document.querySelector('.titlebox > h1').innerHTML;
6978 $(benefitsForm).html('<form action="/post/friendnote" id="friendnote-r9_2vt1" method="post" class="pretty-form medium-text friend-note" onsubmit="return post_form(this, \'friendnote\');"><input type="hidden" name="name" value="'+thisUser+'"><input type="text" maxlength="300" name="note" id="benefits" class="tiny" onfocus="$(this).parent().addClass(\'edited\')" value="'+thisFriendComment+'"><button onclick="$(this).parent().removeClass(\'edited\')" type="submit">submit</button><span class="status"></span></form>');
6979 insertAfter( friendButton, benefitsForm );
6985 attachVoteHandlers: function(obj) {
6986 var voteButtons = obj.querySelectorAll('.arrow');
6987 this.voteStates = [];
6988 for (var i=0, len=voteButtons.length;i<len;i++) {
6989 // get current vote states so that when we listen, we check the delta...
6990 // pairNum is just the index of the "pair" of vote arrows... it's i/2 with no remainder...
6991 var pairNum = Math.floor(i/2);
6992 if (typeof this.voteStates[pairNum] === 'undefined') {
6993 this.voteStates[pairNum] = 0;
6995 if (voteButtons[i].classList.contains('upmod')) {
6996 this.voteStates[pairNum] = 1;
6997 } else if (voteButtons[i].classList.contains('downmod')) {
6998 this.voteStates[pairNum] = -1;
7000 // add an event listener to vote buttons to track votes, but only if we're logged in....
7001 voteButtons[i].setAttribute('pairNum',pairNum);
7002 if (RESUtils.loggedInUser()) {
7003 voteButtons[i].addEventListener('click', modules['userTagger'].handleVoteClick, true);
7007 handleVoteClick: function(e) {
7008 var tags = RESStorage.getItem('RESmodules.userTagger.tags');
7009 if (typeof tags !== 'undefined') modules['userTagger'].tags = safeJSON.parse(tags, 'RESmodules.userTagger.tags', true);
7010 if (e.target.getAttribute('onclick').indexOf('unvotable') === -1) {
7011 var pairNum = e.target.getAttribute('pairNum');
7012 if (pairNum) pairNum = parseInt(pairNum, 10);
7013 var thisAuthorA = this.parentNode.nextSibling.querySelector('p.tagline a.author');
7014 // if this is a post with a thumbnail, we need to adjust the query a bit...
7015 if (thisAuthorA === null && this.parentNode.nextSibling.classList.contains('thumbnail')) {
7016 thisAuthorA = this.parentNode.nextSibling.nextSibling.querySelector('p.tagline a.author');
7019 var thisVWobj = this.parentNode.nextSibling.querySelector('.voteWeight');
7020 if (!thisVWobj) thisVWobj = this.parentNode.parentNode.querySelector('.voteWeight');
7021 // but what if no obj exists
7022 var thisAuthor = thisAuthorA.textContent;
7024 if (typeof modules['userTagger'].tags[thisAuthor] !== 'undefined') {
7025 if (typeof modules['userTagger'].tags[thisAuthor].votes !== 'undefined') {
7026 votes = parseInt(modules['userTagger'].tags[thisAuthor].votes, 10);
7029 modules['userTagger'].tags[thisAuthor] = {};
7031 // there are 6 possibilities here:
7032 // 1) no vote yet, click upmod
7033 // 2) no vote yet, click downmod
7034 // 3) already upmodded, undoing
7035 // 4) already downmodded, undoing
7036 // 5) upmodded before, switching to downmod
7037 // 6) downmodded before, switching to upmod
7038 var upOrDown = ((this.classList.contains('up')) || (this.classList.contains('upmod'))) ? 'up' : 'down';
7039 // did they click the up arrow, or down arrow?
7042 // the class changes BEFORE the click event is triggered, so we have to look at them backwards.
7043 // if the arrow now has class "up" instead of "upmod", then it was "upmod" before, which means
7044 // we are undoing an upvote...
7045 if (this.classList.contains('up')) {
7046 // this is an undo of an upvote. subtract one from votes. We end on no vote.
7048 modules['userTagger'].voteStates[pairNum] = 0;
7050 // They've upvoted... the question is, is it an upvote alone, or an an undo of a downvote?
7051 // add one vote either way...
7053 // if it was previously downvoted, add another!
7054 if (modules['userTagger'].voteStates[pairNum] === -1) {
7057 modules['userTagger'].voteStates[pairNum] = 1;
7061 // the class changes BEFORE the click event is triggered, so we have to look at them backwards.
7062 // if the arrow now has class "up" instead of "upmod", then it was "upmod" before, which means
7063 // we are undoing an downvote...
7064 if (this.classList.contains('down')) {
7065 // this is an undo of an downvote. subtract one from votes. We end on no vote.
7067 modules['userTagger'].voteStates[pairNum] = 0;
7069 // They've downvoted... the question is, is it an downvote alone, or an an undo of an upvote?
7070 // subtract one vote either way...
7072 // if it was previously upvoted, subtract another!
7073 if (modules['userTagger'].voteStates[pairNum] === 1) {
7076 modules['userTagger'].voteStates[pairNum] = -1;
7081 if ((hasClass(this, 'upmod')) || (hasClass(this, 'down'))) {
7082 // upmod = upvote. down = undo of downvote.
7084 } else if ((hasClass(this, 'downmod')) || (hasClass(this, 'up'))) {
7085 // downmod = downvote. up = undo of downvote.
7089 modules['userTagger'].tags[thisAuthor].votes = votes;
7090 RESStorage.setItem('RESmodules.userTagger.tags', JSON.stringify(modules['userTagger'].tags));
7091 modules['userTagger'].colorUser(thisVWobj, thisAuthor, votes);
7095 drawUserTagTable: function(sortMethod, descending) {
7096 this.currentSortMethod = sortMethod || this.currentSortMethod;
7097 this.descending = (descending == null) ? this.descending : descending == true;
7098 var taggedUsers = [];
7099 var filterType = $('#tagFilter').val();
7100 for (var i in this.tags) {
7101 if (filterType === 'tagged users') {
7102 if (typeof this.tags[i].tag !== 'undefined') taggedUsers.push(i);
7104 taggedUsers.push(i);
7107 switch (this.currentSortMethod) {
7109 taggedUsers.sort(function(a,b) {
7110 var tagA = (typeof modules['userTagger'].tags[a].tag === 'undefined') ? 'zzzzz' : modules['userTagger'].tags[a].tag.toLowerCase();
7111 var tagB = (typeof modules['userTagger'].tags[b].tag === 'undefined') ? 'zzzzz' : modules['userTagger'].tags[b].tag.toLowerCase();
7112 return (tagA > tagB) ? 1 : (tagB > tagA) ? -1 : 0;
7114 if (this.descending) taggedUsers.reverse();
7117 taggedUsers.sort(function(a,b) {
7118 var tagA = (typeof modules['userTagger'].tags[a].ignore === 'undefined') ? 'z' : 'a';
7119 var tagB = (typeof modules['userTagger'].tags[b].ignore === 'undefined') ? 'z' : 'a';
7120 return (tagA > tagB) ? 1 : (tagB > tagA) ? -1 : 0;
7122 if (this.descending) taggedUsers.reverse();
7125 taggedUsers.sort(function(a,b) {
7126 var colorA = (typeof modules['userTagger'].tags[a].color === 'undefined') ? 'zzzzz' : modules['userTagger'].tags[a].color.toLowerCase();
7127 var colorB = (typeof modules['userTagger'].tags[b].color === 'undefined') ? 'zzzzz' : modules['userTagger'].tags[b].color.toLowerCase();
7128 return (colorA > colorB) ? 1 : (colorB > colorA) ? -1 : 0;
7130 if (this.descending) taggedUsers.reverse();
7133 taggedUsers.sort(function(a,b) {
7134 var tagA = (typeof modules['userTagger'].tags[a].votes === 'undefined') ? 0 : modules['userTagger'].tags[a].votes;
7135 var tagB = (typeof modules['userTagger'].tags[b].votes === 'undefined') ? 0 : modules['userTagger'].tags[b].votes;
7136 return (tagA > tagB) ? 1 : (tagB > tagA) ? -1 : (a.toLowerCase() > b.toLowerCase());
7138 if (this.descending) taggedUsers.reverse();
7141 // sort users, ignoring case
7142 taggedUsers.sort(function(a,b) {
7143 return (a.toLowerCase() > b.toLowerCase()) ? 1 : (b.toLowerCase() > a.toLowerCase()) ? -1 : 0;
7145 if (this.descending) taggedUsers.reverse();
7148 $('#userTaggerTable tbody').html('');
7149 var tagsPerPage = parseInt(modules['dashboard'].options['tagsPerPage'].value, 10);
7150 var count = taggedUsers.length;
7155 var tagControls = $('#tagPageControls');
7156 var page = tagControls.prop('page');
7157 var pages = Math.ceil(count / tagsPerPage);
7158 page = Math.min(page, pages);
7159 page = Math.max(page, 1);
7160 tagControls.prop('page', page).prop('pageCount', pages);
7161 tagControls.find('.RESGalleryLabel').text(page + ' of ' + pages);
7162 start = tagsPerPage*(page-1);
7163 end = Math.min(count, tagsPerPage*page);
7166 for (var i = start; i < end; i++) {
7167 var thisUser = taggedUsers[i];
7168 var thisTag = (typeof this.tags[thisUser].tag === 'undefined') ? '' : this.tags[thisUser].tag;
7169 var thisVotes = (typeof this.tags[thisUser].votes === 'undefined') ? 0 : this.tags[thisUser].votes;
7170 var thisColor = (typeof this.tags[thisUser].color === 'undefined') ? '' : this.tags[thisUser].color;
7171 var thisIgnore = (typeof this.tags[thisUser].ignore === 'undefined') ? 'no' : 'yes';
7173 var userTagLink = document.createElement('a');
7174 if (thisTag === '') {
7175 // thisTag = '<div class="RESUserTagImage"></div>';
7176 userTagLink.setAttribute('class','userTagLink RESUserTagImage');
7178 userTagLink.setAttribute('class','userTagLink hasTag');
7180 $(userTagLink).html(escapeHTML(thisTag));
7182 var bgColor = (thisColor === 'none') ? 'transparent' : thisColor;
7183 userTagLink.setAttribute('style','background-color: '+bgColor+'; color: '+this.bgToTextColorMap[thisColor]+' !important;');
7185 userTagLink.setAttribute('username',thisUser);
7186 userTagLink.setAttribute('title','set a tag');
7187 userTagLink.setAttribute('href','javascript:void(0)');
7188 userTagLink.addEventListener('click', function(e) {
7189 modules['userTagger'].openUserTagPrompt(e.target, this.getAttribute('username'));
7192 $('#userTaggerTable tbody').append('<tr><td><a class="author" href="/user/'+thisUser+'">'+thisUser+'</a> <span class="deleteButton" user="'+thisUser+'"></span></td><td id="tag_'+i+'"></td><td id="ignore_'+i+'">'+thisIgnore+'</td><td><span style="color: '+thisColor+'">'+thisColor+'</span></td><td>'+thisVotes+'</td></tr>');
7193 $('#tag_'+i).append(userTagLink);
7195 $('#userTaggerTable tbody .deleteButton').click(function(e) {
7196 var thisUser = $(this).attr('user');
7197 var answer = confirm("Are you sure you want to delete the tag for user: "+thisUser+"?");
7199 delete modules['userTagger'].tags[thisUser];
7200 RESStorage.setItem('RESmodules.userTagger.tags', JSON.stringify(modules['userTagger'].tags));
7201 $(this).closest('tr').remove();
7205 saveTagForm: function() {
7206 var thisName = document.getElementById('userTaggerName').value;
7207 var thisTag = document.getElementById('userTaggerTag').value;
7208 var thisColor = document.getElementById('userTaggerColor').value;
7209 var thisIgnore = document.getElementById('userTaggerIgnore').checked;
7210 var thisLink = document.getElementById('userTaggerLink').value;
7211 var thisVotes = parseInt(document.getElementById('userTaggerVoteWeight').value, 10);
7212 if (isNaN(thisVotes)) thisVotes = 0;
7213 modules['userTagger'].setUserTag(thisName, thisTag, thisColor, thisIgnore, thisLink, thisVotes);
7236 openUserTagPrompt: function(obj, username) {
7237 var thisXY=RESUtils.getXYpos(obj);
7238 this.clickedTag = obj;
7239 var thisH3 = document.querySelector('#userTaggerToolTip h3');
7240 thisH3.textContent = 'Tag '+username;
7241 document.getElementById('userTaggerName').value = username;
7243 var thisIgnore = null;
7244 if (typeof this.tags[username] !== 'undefined') {
7245 if (typeof this.tags[username].link !== 'undefined') {
7246 document.getElementById('userTaggerLink').value = this.tags[username].link;
7248 document.getElementById('userTaggerLink').value = '';
7250 if (typeof this.tags[username].tag !== 'undefined') {
7251 document.getElementById('userTaggerTag').value = this.tags[username].tag;
7253 document.getElementById('userTaggerTag').value = '';
7254 if (typeof this.tags[username].link === 'undefined') {
7255 // since we haven't yet set a tag or a link for this user, auto populate a link for the
7256 // user based on where we are tagging from.
7257 this.setLinkBasedOnTagLocation(obj);
7260 if (typeof this.tags[username].ignore !== 'undefined') {
7261 document.getElementById('userTaggerIgnore').checked = this.tags[username].ignore;
7262 var thisToggle = document.getElementById('userTaggerIgnoreContainer');
7263 if (this.tags[username].ignore) thisToggle.classList.add('enabled');
7265 document.getElementById('userTaggerIgnore').checked = false;
7267 if (typeof this.tags[username].votes !== 'undefined') {
7268 document.getElementById('userTaggerVoteWeight').value = this.tags[username].votes;
7270 document.getElementById('userTaggerVoteWeight').value = '';
7272 if (typeof this.tags[username].color !== 'undefined') {
7273 RESUtils.setSelectValue(document.getElementById('userTaggerColor'), this.tags[username].color);
7275 document.getElementById('userTaggerColor').selectedIndex = 0;
7278 document.getElementById('userTaggerTag').value = '';
7279 document.getElementById('userTaggerIgnore').checked = false;
7280 document.getElementById('userTaggerVoteWeight').value = '';
7281 document.getElementById('userTaggerLink').value = '';
7282 if (this.options.storeSourceLink.value) {
7283 this.setLinkBasedOnTagLocation(obj);
7285 document.getElementById('userTaggerColor').selectedIndex = 0;
7287 this.userTaggerToolTip.setAttribute('style', 'display: block; top: ' + thisXY.y + 'px; left: ' + thisXY.x + 'px;');
7288 document.getElementById('userTaggerTag').focus();
7289 modules['userTagger'].updateTagPreview();
7292 setLinkBasedOnTagLocation: function(obj) {
7293 var closestEntry = $(obj).closest('.entry');
7294 var linkTitle = $(closestEntry).find('a.title');
7295 // if we didn't find anything, try a new search (works on inbox)
7296 if (!linkTitle.length) {
7297 linkTitle = $(closestEntry).find('a.bylink');
7299 if (linkTitle.length) {
7300 document.getElementById('userTaggerLink').value = $(linkTitle).attr('href');
7302 var permaLink = $(closestEntry).find('.flat-list.buttons li.first a');
7303 if (permaLink.length) {
7304 document.getElementById('userTaggerLink').value = $(permaLink).attr('href');
7308 updateTagPreview: function() {
7309 $(modules['userTagger'].userTaggerPreview).text(modules['userTagger'].userTaggerTag.value);
7310 var bgcolor = modules['userTagger'].userTaggerColor[modules['userTagger'].userTaggerColor.selectedIndex].value;
7311 modules['userTagger'].userTaggerPreview.style.backgroundColor = bgcolor;
7312 modules['userTagger'].userTaggerPreview.style.color = modules['userTagger'].bgToTextColorMap[bgcolor];
7314 closeUserTagPrompt: function() {
7315 this.userTaggerToolTip.setAttribute('style','display: none');
7316 if (modules['keyboardNav'].isEnabled()) {
7317 var inputs = this.userTaggerToolTip.querySelectorAll('INPUT, BUTTON');
7318 // remove focus from any input fields from the prompt so that keyboard navigation works again...
7319 for (var i=0,len=inputs.length; i<len; i++) {
7324 setUserTag: function(username, tag, color, ignore, link, votes, noclick) {
7325 if (((tag !== null) && (tag !== '')) || (ignore)) {
7326 if (tag === '') tag = 'ignored';
7327 if (typeof this.tags[username] === 'undefined') this.tags[username] = {};
7328 this.tags[username].tag = tag;
7329 this.tags[username].link = link;
7330 this.tags[username].color = color;
7331 var bgColor = (color === "none") ? "transparent" : color;
7333 this.tags[username].ignore = true;
7335 delete this.tags[username].ignore;
7338 this.clickedTag.setAttribute('class', 'userTagLink hasTag');
7339 this.clickedTag.setAttribute('style', 'background-color: '+bgColor+'; color: ' + this.bgToTextColorMap[color]+' !important;');
7340 $(this.clickedTag).html(escapeHTML(tag));
7343 if (typeof this.tags[username] !== 'undefined') {
7344 delete this.tags[username].tag;
7345 delete this.tags[username].color;
7346 delete this.tags[username].link;
7347 if (this.tags[username].tag === 'ignored') delete this.tags[username].tag;
7348 delete this.tags[username].ignore;
7351 this.clickedTag.setAttribute('style', 'background-color: transparent');
7352 this.clickedTag.setAttribute('class', 'userTagLink RESUserTagImage');
7353 $(this.clickedTag).html('');
7357 if (typeof this.tags[username] !== 'undefined') {
7358 this.tags[username].votes = (isNaN(votes)) ? 0 : votes;
7361 var thisVW = this.clickedTag.parentNode.parentNode.querySelector('a.voteWeight');
7363 this.colorUser(thisVW, username, votes);
7366 if (RESUtils.isEmpty(this.tags[username])) delete this.tags[username];
7367 RESStorage.setItem('RESmodules.userTagger.tags', JSON.stringify(this.tags));
7368 this.closeUserTagPrompt();
7370 applyTags: function(ele) {
7371 if (ele == null) ele = document;
7372 var authors = ele.querySelectorAll('.noncollapsed a.author, p.tagline a.author, #friend-table span.user a, .sidecontentbox .author, div.md a[href^="/u/"]:not([href*="/m/"]), div.md a[href*="reddit.com/u/"]:not([href*="/m/"]), .usertable a.author');
7373 RESUtils.forEachChunked(authors, 15, 1000, function(arrayElement, index, array) {
7374 modules['userTagger'].applyTagToAuthor(arrayElement);
7377 applyTagToAuthor: function(thisAuthorObj) {
7378 var userObject = [];
7379 // var thisAuthorObj = this.authors[authorNum];
7380 if ((thisAuthorObj) && (!(thisAuthorObj.classList.contains('userTagged'))) && (typeof thisAuthorObj !== 'undefined') && (thisAuthorObj !== null)) {
7381 if (this.options.hoverInfo.value) {
7382 // add event listener to hover, so we can grab user data on hover...
7383 thisAuthorObj.addEventListener('mouseover', function(e) {
7384 modules['userTagger'].showTimer = setTimeout(function() {
7385 modules['userTagger'].showAuthorInfo(thisAuthorObj);
7386 }, modules['userTagger'].options.hoverDelay.value);
7388 thisAuthorObj.addEventListener('mouseout', function(e) {
7389 clearTimeout(modules['userTagger'].showTimer);
7391 thisAuthorObj.addEventListener('click', function(e) {
7392 clearTimeout(modules['userTagger'].showTimer);
7395 var test = thisAuthorObj.href.match(this.usernameRE);
7396 if (test) var thisAuthor = test[1];
7397 // var thisAuthor = thisAuthorObj.text;
7399 if ((thisAuthor) && (thisAuthor.substr(0,3) === '/u/')) {
7401 thisAuthor = thisAuthor.substr(3);
7404 thisAuthorObj.classList.add('userTagged');
7405 if (typeof userObject[thisAuthor] === 'undefined') {
7408 var thisColor = null;
7409 var thisIgnore = null;
7410 if ((this.tags !== null) && (typeof this.tags[thisAuthor] !== 'undefined')) {
7411 if (typeof this.tags[thisAuthor].votes !== 'undefined') {
7412 thisVotes = parseInt(this.tags[thisAuthor].votes, 10);
7414 if (typeof this.tags[thisAuthor].tag !== 'undefined') {
7415 thisTag = this.tags[thisAuthor].tag;
7417 if (typeof this.tags[thisAuthor].color !== 'undefined') {
7418 thisColor = this.tags[thisAuthor].color;
7420 if (typeof this.tags[thisAuthor].ignore !== 'undefined') {
7421 thisIgnore = this.tags[thisAuthor].ignore;
7424 userObject[thisAuthor] = {
7432 var userTagFrag = document.createDocumentFragment();
7434 var userTagLink = document.createElement('a');
7436 // thisTag = '<div class="RESUserTagImage"></div>';
7437 userTagLink.setAttribute('class','userTagLink RESUserTagImage');
7439 userTagLink.setAttribute('class','userTagLink hasTag');
7441 $(userTagLink).html(escapeHTML(thisTag));
7443 var bgColor = (thisColor === 'none') ? 'transparent' : thisColor;
7444 userTagLink.setAttribute('style','background-color: '+bgColor+'; color: '+this.bgToTextColorMap[thisColor]+' !important;');
7446 userTagLink.setAttribute('username',thisAuthor);
7447 userTagLink.setAttribute('title','set a tag');
7448 userTagLink.setAttribute('href','javascript:void(0)');
7449 userTagLink.addEventListener('click', function(e) {
7450 modules['userTagger'].openUserTagPrompt(e.target, this.getAttribute('username'));
7452 var userTag = document.createElement('span');
7453 userTag.classList.add('RESUserTag');
7454 // var lp = document.createTextNode(' (');
7455 // var rp = document.createTextNode(')');
7456 userTag.appendChild(userTagLink);
7457 // userTagFrag.appendChild(lp);
7458 userTagFrag.appendChild(userTag);
7459 // userTagFrag.appendChild(rp);
7460 if (this.options.colorUser.value) {
7461 var userVoteFrag = document.createDocumentFragment();
7462 var spacer = document.createTextNode(' ');
7463 userVoteFrag.appendChild(spacer);
7464 var userVoteWeight = document.createElement('a');
7465 userVoteWeight.setAttribute('href','javascript:void(0)');
7466 userVoteWeight.setAttribute('class','voteWeight');
7467 $(userVoteWeight).text('[vw]');
7468 userVoteWeight.addEventListener('click', function(e) {
7469 var theTag = this.parentNode.querySelector('.userTagLink');
7470 modules['userTagger'].openUserTagPrompt(theTag, theTag.getAttribute('username'));
7472 this.colorUser(userVoteWeight, thisAuthor, userObject[thisAuthor].votes);
7473 userVoteFrag.appendChild(userVoteWeight);
7474 userTagFrag.appendChild(userVoteFrag);
7476 insertAfter( thisAuthorObj, userTagFrag );
7477 thisIgnore = userObject[thisAuthor].ignore;
7478 if (thisIgnore && (RESUtils.pageType() !== 'profile')) {
7479 if (this.options.hardIgnore.value) {
7480 if (RESUtils.pageType() === 'comments') {
7481 var thisComment = thisAuthorObj.parentNode.parentNode.querySelector('.usertext');
7483 $(thisComment).textContent = thisAuthor + ' is an ignored user';
7484 thisComment.classList.add('ignoredUserComment');
7486 var toggle = thisComment.parentNode.querySelector('a.expand');
7487 RESUtils.click(toggle);
7489 // firefox fails when we use this jquery call, so we're ditching it
7490 // in favor of the above lines (grabbing toggle, using RESUtils.click...)
7491 // $(thisComment).parent().find('a.expand').click();
7493 var thisPost = thisAuthorObj.parentNode.parentNode.parentNode;
7494 // hide post block first...
7495 thisPost.style.display = 'none';
7496 // hide associated voting block...
7497 if (thisPost.previousSibling) {
7498 thisPost.previousSibling.style.display = 'none';
7502 if (RESUtils.pageType() === 'comments') {
7503 var thisComment = thisAuthorObj.parentNode.parentNode.querySelector('.usertext');
7505 thisComment.textContent = thisAuthor + ' is an ignored user';
7506 thisComment.classList.add('ignoredUserComment');
7509 var thisPost = thisAuthorObj.parentNode.parentNode.parentNode.querySelector('p.title');
7511 // need this setTimeout, potentially because destroying the innerHTML causes conflict with other modules?
7512 setTimeout(function() {
7513 thisPost.textContent = thisAuthor + ' is an ignored user';
7515 thisPost.setAttribute('class','ignoredUserPost');
7523 colorUser: function(obj, author, votes) {
7524 if (this.options.colorUser.value) {
7525 votes = parseInt(votes, 10);
7529 var voteString = '+';
7531 red = Math.max(0, (255-(8*votes)));
7533 blue = Math.max(0, (255-(8*votes)));
7534 } else if (votes < 0) {
7536 green = Math.max(0, (255-Math.abs(8*votes)));
7537 blue = Math.max(0, (255-Math.abs(8*votes)));
7540 voteString = voteString + votes;
7541 var rgb='rgb('+red+','+green+','+blue+')';
7544 obj.style.display = 'none';
7546 obj.style.display = 'inline';
7547 (modules['styleTweaks'].options.lightOrDark.value === 'dark') ? obj.style.color = rgb : obj.style.backgroundColor = rgb;
7548 if (this.options.vwNumber.value) obj.textContent = '[' + voteString + ']';
7549 if (this.options.vwTooltip.value) obj.setAttribute('title','your votes for '+escapeHTML(author)+': '+escapeHTML(voteString));
7554 showAuthorInfo: function(obj) {
7555 var isFriend = obj.classList.contains('friend');
7556 var thisXY=RESUtils.getXYpos(obj);
7557 var thisWidth = $(obj).width();
7558 // var thisUserName = obj.textContent;
7559 var test = obj.href.match(this.usernameRE);
7560 if (test) var thisUserName = test[1];
7561 // if (thisUserName.substr(0,3) === '/u/') thisUserName = thisUserName.substr(3);
7562 $(this.authorInfoToolTipHeader).html('<a href="/user/'+escapeHTML(thisUserName)+'">' + escapeHTML(thisUserName) + '</a> (<a href="/user/'+escapeHTML(thisUserName)+'/submitted/">Links</a>) (<a href="/user/'+escapeHTML(thisUserName)+'/comments/">Comments</a>)');
7563 RESUtils.getUserInfo(function(userInfo) {
7564 var myID = 't2_'+userInfo.data.id;
7566 var friendButton = '<span class="fancy-toggle-button toggle" style="display: inline-block; margin-left: 12px;"><a class="option active remove" href="#" tabindex="100" onclick="return toggle(this, unfriend(\''+obj.textContent+'\', \''+myID+'\', \'friend\'), friend(\''+obj.textContent+'\', \''+myID+'\', \'friend\'))">- friends</a><a class="option add" href="#">+ friends</a></span>';
7568 var friendButton = '<span class="fancy-toggle-button toggle" style="display: inline-block; margin-left: 12px;"><a class="option active add" href="#" tabindex="100" onclick="return toggle(this, friend(\''+obj.textContent+'\', \''+myID+'\', \'friend\'), unfriend(\''+obj.textContent+'\', \''+myID+'\', \'friend\'))">+ friends</a><a class="option remove" href="#">- friends</a></span>';
7570 var friendButtonEle = $(friendButton);
7571 $(modules['userTagger'].authorInfoToolTipHeader).append(friendButtonEle);
7573 $(this.authorInfoToolTipContents).html('<a class="hoverAuthor" href="/user/'+escapeHTML(thisUserName)+'">'+escapeHTML(thisUserName)+'</a>:<br><img src="'+RESConsole.loader+'"> loading...');
7574 if((window.innerWidth-thisXY.x-thisWidth)<=450){
7575 // tooltip would go off right edge - reverse it.
7576 this.authorInfoToolTip.classList.add('right');
7577 var tooltipWidth = $(this.authorInfoToolTip).width();
7578 this.authorInfoToolTip.setAttribute('style', 'top: ' + (thisXY.y - 14) + 'px; left: ' + (thisXY.x - tooltipWidth - 30) + 'px;');
7580 this.authorInfoToolTip.classList.remove('right');
7581 this.authorInfoToolTip.setAttribute('style', 'top: ' + (thisXY.y - 14) + 'px; left: ' + (thisXY.x + thisWidth + 25) + 'px;');
7583 if(this.options.fadeSpeed.value < 0 || this.options.fadeSpeed.value > 1 || isNaN(this.options.fadeSpeed.value)) {
7584 this.options.fadeSpeed.value = 0.3;
7586 RESUtils.fadeElementIn(this.authorInfoToolTip, this.options.fadeSpeed.value);
7587 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'authorInfo');
7588 setTimeout(function() {
7589 if (!RESUtils.elementUnderMouse(modules['userTagger'].authorInfoToolTip) && (!RESUtils.elementUnderMouse(obj))) {
7590 modules['userTagger'].hideAuthorInfo();
7593 obj.addEventListener('mouseout', modules['userTagger'].delayedHideAuthorInfo);
7594 if (typeof this.authorInfoCache[thisUserName] !== 'undefined') {
7595 this.writeAuthorInfo(this.authorInfoCache[thisUserName], obj);
7599 url: location.protocol + "//"+location.hostname+"/user/" + thisUserName + "/about.json?app=res",
7600 onload: function(response) {
7601 var thisResponse = JSON.parse(response.responseText);
7602 modules['userTagger'].authorInfoCache[thisUserName] = thisResponse;
7603 modules['userTagger'].writeAuthorInfo(thisResponse, obj);
7608 delayedHideAuthorInfo: function(e) {
7609 modules['userTagger'].hideTimer = setTimeout(function() {
7610 e.target.removeEventListener('mouseout', modules['userTagger'].delayedHideAuthorInfo);
7611 modules['userTagger'].hideAuthorInfo();
7612 }, modules['userTagger'].options.fadeDelay.value);
7614 writeAuthorInfo: function(jsonData, authorLink) {
7615 if (!jsonData.data) {
7616 $(this.authorInfoToolTipContents).text("User not found");
7619 var utctime = jsonData.data.created_utc;
7620 var d = new Date(utctime * 1000);
7621 // var userHTML = '<a class="hoverAuthor" href="/user/'+jsonData.data.name+'">'+jsonData.data.name+'</a>:';
7622 var userHTML = '<div class="authorFieldPair"><div class="authorLabel">Redditor since:</div> <div class="authorDetail">' + RESUtils.niceDate(d, this.options.USDateFormat.value) + ' (' + RESUtils.niceDateDiff(d) + ')</div></div>';
7623 userHTML += '<div class="authorFieldPair"><div class="authorLabel">Link Karma:</div> <div class="authorDetail">' + escapeHTML(jsonData.data.link_karma) + '</div></div>';
7624 userHTML += '<div class="authorFieldPair"><div class="authorLabel">Comment Karma:</div> <div class="authorDetail">' + escapeHTML(jsonData.data.comment_karma) + '</div></div>';
7625 if ((typeof modules['userTagger'].tags[jsonData.data.name] !== 'undefined') && (modules['userTagger'].tags[jsonData.data.name].link)) {
7626 userHTML += '<div class="authorFieldPair"><div class="authorLabel">Link:</div> <div class="authorDetail"><a target="_blank" href="'+escapeHTML(modules['userTagger'].tags[jsonData.data.name].link)+'">website link</a></div></div>';
7628 userHTML += '<div class="clear"></div><div class="bottomButtons">';
7629 userHTML += '<a target="_blank" class="blueButton" href="http://www.reddit.com/message/compose/?to='+escapeHTML(jsonData.data.name)+'"><img src="http://www.redditstatic.com/mailgray.png"> send message</a>';
7630 if (jsonData.data.is_gold) {
7631 userHTML += '<a target="_blank" class="blueButton" href="http://www.reddit.com/gold/about">User has Reddit Gold</a>';
7633 userHTML += '<a target="_blank" id="gildUser" class="blueButton" href="http://www.reddit.com/gold?goldtype=gift&recipient='+escapeHTML(jsonData.data.name)+'">Gift Reddit Gold</a>';
7636 if (this.options.highlightButton.value) {
7637 if (!this.highlightedUsers || !this.highlightedUsers[jsonData.data.name]) {
7638 userHTML += '<div class="blueButton" id="highlightUser" user="'+escapeHTML(jsonData.data.name)+'">Highlight</div>';
7640 userHTML += '<div class="redButton" id="highlightUser" user="'+escapeHTML(jsonData.data.name)+'">Unhighlight</div>';
7644 if ((modules['userTagger'].tags[jsonData.data.name]) && (modules['userTagger'].tags[jsonData.data.name].ignore)) {
7645 userHTML += '<div class="redButton" id="ignoreUser" user="'+escapeHTML(jsonData.data.name)+'">∅ Unignore</div>';
7647 userHTML += '<div class="blueButton" id="ignoreUser" user="'+escapeHTML(jsonData.data.name)+'">∅ Ignore</div>';
7649 userHTML += '<div class="clear"></div></div>'; // closes bottomButtons div
7650 $(this.authorInfoToolTipContents).html(userHTML);
7651 this.authorInfoToolTipIgnore = this.authorInfoToolTipContents.querySelector('#ignoreUser');
7652 this.authorInfoToolTipIgnore.addEventListener('click', modules['userTagger'].ignoreUser, false);
7653 if (modules['userTagger'].options.highlightButton.value) {
7654 this.authorInfoToolTipHighlight = this.authorInfoToolTipContents.querySelector('#highlightUser');
7655 if (this.authorInfoToolTipHighlight) {
7656 this.authorInfoToolTipHighlight.addEventListener('click', function(e) {
7657 var username = e.target.getAttribute('user');
7658 modules['userTagger'].toggleUserHighlight(username);
7662 if (modules['userTagger'].options.gildComments.value && RESUtils.pageType() === 'comments') {
7663 var giveGold = this.authorInfoToolTipContents.querySelector('#gildUser');
7664 giveGold && giveGold.addEventListener('click', function(e) {
7665 if (e.ctrlKey || e.cmdKey || e.shiftKey) return;
7667 var comment = $(authorLink).closest('.comment');
7668 if (!comment) return;
7670 modules['userTagger'].hideAuthorInfo();
7671 var giveGold = comment.find('.give-gold')[0];
7672 RESUtils.click(giveGold);
7677 toggleUserHighlight: function(username) {
7678 if (!this.highlightedUsers) this.highlightedUsers = {};
7680 if (this.highlightedUsers[username]) {
7681 this.highlightedUsers[username].remove();
7682 delete this.highlightedUsers[username];
7683 this.toggleUserHighlightButton(true);
7686 this.highlightedUsers[username] =
7687 modules['userHighlight'].highlightUser(username);
7688 this.toggleUserHighlightButton(false);
7691 toggleUserHighlightButton: function(canHighlight) {
7692 $(this.authorInfoToolTipHighlight)
7693 .toggleClass('blueButton', canHighlight)
7694 .toggleClass('redButton', !canHighlight)
7695 .text(canHighlight ? 'Highlight' : 'Unhighlight');
7697 ignoreUser: function(e) {
7698 if (e.target.classList.contains('blueButton')) {
7699 e.target.classList.remove('blueButton');
7700 e.target.classList.add('redButton');
7701 $(e.target).html('∅ Unignore');
7702 var thisIgnore = true;
7704 e.target.classList.remove('redButton');
7705 e.target.classList.add('blueButton');
7706 $(e.target).html('∅ Ignore');
7707 var thisIgnore = false;
7709 var thisName = e.target.getAttribute('user');
7710 var thisColor, thisLink, thisVotes, thisTag;
7711 if (modules['userTagger'].tags[thisName]) {
7712 thisColor = modules['userTagger'].tags[thisName].color || '';
7713 thisLink = modules['userTagger'].tags[thisName].link || '';
7714 thisVotes = modules['userTagger'].tags[thisName].votes || 0;
7715 thisTag = modules['userTagger'].tags[thisName].tag || '';
7717 if ((thisIgnore) && (thisTag === '')) {
7718 thisTag = 'ignored';
7719 } else if ((!thisIgnore) && (thisTag === 'ignored')) {
7722 modules['userTagger'].setUserTag(thisName, thisTag, thisColor, thisIgnore, thisLink, thisVotes, true); // last true is for noclick param
7724 hideAuthorInfo: function(obj) {
7725 // this.authorInfoToolTip.setAttribute('style', 'display: none');
7726 if(this.options.fadeSpeed.value < 0 || this.options.fadeSpeed.value > 1 || isNaN(this.options.fadeSpeed.value)) {
7727 this.options.fadeSpeed.value = 0.3;
7729 RESUtils.fadeElementOut(this.authorInfoToolTip, this.options.fadeSpeed.value);
7730 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'authorInfo');
7732 updateTagStorage: function() {
7733 // update tag storage format from the old individual bits to a big JSON blob
7734 // It's OK that we're directly accessing localStorage here because if they have old school tag storage, it IS in localStorage.
7735 ls = (typeof unsafeWindow !== 'undefined') ? unsafeWindow.localStorage : localStorage;
7738 for (var i = 0, len=ls.length; i < len; i++){
7739 var keySplit = null;
7740 if (ls.key(i)) keySplit = ls.key(i).split('.');
7742 var keyRoot = keySplit[0];
7745 var thisNode = keySplit[1];
7746 if (typeof tags[keySplit[2]] === 'undefined') {
7747 tags[keySplit[2]] = {};
7749 if (thisNode === 'votes') {
7750 tags[keySplit[2]].votes = ls.getItem(ls.key(i));
7751 } else if (thisNode === 'tag') {
7752 tags[keySplit[2]].tag = ls.getItem(ls.key(i));
7753 } else if (thisNode === 'color') {
7754 tags[keySplit[2]].color = ls.getItem(ls.key(i));
7755 } else if (thisNode === 'ignore') {
7756 tags[keySplit[2]].ignore = ls.getItem(ls.key(i));
7758 // now delete the old stored garbage...
7759 var keyString = 'reddituser.'+thisNode+'.'+keySplit[2];
7760 toRemove.push(keyString);
7763 // console.log('Not currently handling keys with root: ' + keyRoot);
7769 RESStorage.setItem('RESmodules.userTagger.tags', JSON.stringify(this.tags));
7770 // now remove the old garbage...
7771 for (var i=0, len=toRemove.length; i<len; i++) {
7772 ls.removeItem(toRemove[i]);
7778 modules['betteReddit'] = {
7779 moduleID: 'betteReddit',
7780 moduleName: 'betteReddit',
7786 description: 'add "full comments" link to comment replies, etc'
7790 value: 'full comments',
7791 description: 'text of full comments link'
7793 commentsLinksNewTabs: {
7796 description: 'Open links found in comments in a new tab'
7801 description: 'Make "save" links change to "unsave" links when clicked'
7806 description: 'Make "hide" links change to "unhide" links when clicked, and provide a 5 second delay prior to hiding the link'
7808 searchSubredditByDefault: {
7811 description: 'Search the current subreddit by default when using the search box, instead of all of reddit.'
7816 description: 'Show unread message count next to orangered?'
7818 showUnreadCountInTitle: {
7821 description: 'Show unread message count in page/tab title?'
7823 showUnreadCountInFavicon: {
7826 description: 'Show unread message count in favicon?'
7828 unreadLinksToInbox: {
7831 description: 'Always go to the inbox, not unread messages, when clicking on orangered'
7836 description: 'Show lengths of videos when possible'
7841 description: 'Show upload date of videos when possible'
7846 description: 'Don\'t use Reddit Toolbar when linking to sites that may not function (twitter, youtube and others)'
7851 { name: 'None', value: 'none' },
7852 { name: 'Subreddit Bar only', value: 'sub' },
7853 { name: 'User Bar', value: 'userbar' },
7854 { name: 'Subreddit Bar and User bar', value: 'subanduser' },
7855 { name: 'Full Header', value: 'header' }
7858 description: 'Pin the subreddit bar or header to the top, even when you scroll.'
7863 description: 'Preload selftext data to make selftext expandos faster (preloads after first expando)'
7865 showLastEditedTimestamp: {
7868 description: 'Show the time that a text post/comment was edited, without having to hover the timestamp.'
7873 description: 'The saved tab on pages with the multireddit side bar is now located in that collapsible bar. This will restore it to the header. If you have the \'Save Comments\' module enabled then you\'ll see both the links <em>and</em> comments tabs.'
7876 description: 'Adds a number of interface enhancements to Reddit, such as "full comments" links, the ability to unhide accidentally hidden posts, and more',
7877 isEnabled: function() {
7878 return RESConsole.getModulePrefs(this.moduleID);
7880 isMatchURL: function() {
7881 return RESUtils.isMatchURL(this.moduleID);
7884 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
7887 if ((this.isEnabled()) && (this.isMatchURL())) {
7889 if ((this.options.toolbarFix.value) && ((RESUtils.pageType() === 'linklist') || RESUtils.pageType() === 'comments')) {
7892 if ((RESUtils.pageType() === 'comments') && (this.options.commentsLinksNewTabs.value)) {
7893 this.commentsLinksNewTabs();
7895 // if (((RESUtils.pageType() === 'inbox') || (RESUtils.pageType() === 'profile') || ((RESUtils.pageType() === 'comments') && (RESUtils.currentSubreddit('friends')))) && (this.options.fullCommentsLink.value)) {
7896 // removed profile pages since Reddit does this natively now for those...
7897 if (((RESUtils.pageType() === 'inbox') || ((RESUtils.pageType() === 'comments') && (RESUtils.currentSubreddit('friends') === false))) && (this.options.fullCommentsLink.value)) {
7898 // RESUtils.addCSS('a.redditFullCommentsSub { font-size: 9px !important; color: #BBB !important; }');
7899 this.fullComments();
7901 if ((RESUtils.pageType() === 'profile') && (location.href.split('/').indexOf(RESUtils.loggedInUser()) !== -1)) {
7902 this.editMyComments();
7904 if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (this.options.fixSaveLinks.value)) {
7905 this.fixSaveLinks();
7907 if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (this.options.fixHideLinks.value)) {
7908 this.fixHideLinks();
7910 if ((this.options.turboSelfText.value) && (RESUtils.pageType() === 'linklist')) {
7911 this.setUpTurboSelfText();
7913 if (this.options.showUnreadCountInFavicon.value) {
7914 var faviconDataurl = 'data:image/x-icon;base64,AAABAAIAEBAAAAAAAABoBQAAJgAAACAgAAAAAAAAqAgAAI4FAAAoAAAAEAAAACAAAAABAAgAAAAAAEABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP///wBBRP4Aq5qHAFNOSgCkpv4A4Mu0AA8Q/gCAc2cAzM7+ADYvKQCys7QA/urQAGVo/gCGh4kA1NbYAMWvmgBjZWYA5efoAJucngD//+MAwcPFACIk/gCbinkAycGtADo8PgDz2sIAal5RADAy/gBvcHEA9PbzALijkACMfm8AqqmoALCy/gCAgHsAc2piAMnMzwDBw/4AXVxcAJGUlwDOuKMAR0dHANfDrABLTv4Aubu+ADIzNQBbVlEA++HJAP/23ACho6UAa2RdAJOEdQD3+P4A2tzeAO7UvACEeW4AjY2KALCfjgBsa2oArq6uAM/R0wB2cGwA//DWAKOYiACCg4MAemxfAPDw8ABOSkYA4+PjAMm7pgBeWlYAgHt3AGZhWwBtamQA+t7EAGVbUgB7b2QAoaGgAP7kzADdyLEA9Pf6AP774ACwnIoAZ15VAJiHdwBbXWAAy8vLAIh8cAC0oY0AZ2VkALy+vwD+7dMA9t3FAHJwbgB/f38Aqq2vAP//5wB4bWMA7ta/AHVqXwCCfXsAmYp8AIJ1aQDS0tIApKSkAP7jyQDOuaYA/vjeAH1xZQCBgYEArp2MAJ2enwD5+v4A//LYAH5zaQD13MMAd2xhAJWFdgD+69IA/ujPAGxsbABycXAAtqOPAN/IsQDLu6YA/v7+AP/84QD/+t8A/vfbAP7v1QD64MgA1dfZAH5+fgD//uIA/v7jAP/43QD+990AZVxTAP/x1wD+8tcA/u7UAOTk4wCYiHgA///kAP/+4wD++t8A/vneAP743QD/79UA/u3SAP7s0gD+69EA/unQAPXcxACrra8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA////AD8/Pz8/Pz8/Pz8/Pz8/Pz8/XV1dKY8IYgiPKV1dXV0/P11QVDubPYQ9mztUUF1dP4krRC1+V26Fbld+LUQrXT+UiiV+fiFoQ2ghfn4lil0/P15+fn5+fn5+fn5+fl5dPx8qfn4iHAl+CRwifn4qHz9IHTx+BQcmfiYHBX48HUg/PjYZDh5+fn5+fh4OGTY+P0YzOCBKOTILcCNJIDgzRj8/XV1dfFkXClUQGl1qXV0/P11dXTB4XWdrXV1YRzpdPz9dXV1dXV1vZBhCVlEEXT8/XV1dXV2DY0xtQCRaZl0/P11dXV1dXV1dXV1dXV1dPz8/Pz8/Pz8/Pz8/Pz8/Pz8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAKAAAACAAAABAAAAAAQAIAAAAAACABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD///8AJSr/AJmJegCIjf4AREZIANzFsADGy/4AUFb/ACgjHwBpamsAAAD/AP/mzQCmp6cA2dnaAL29vgDl6P4AYllPAMWzoAAQEhQAMjQ2AHp7fQCxtv4AlZaWAP784AASFv4A7NfAALGgjwCCdGYAy8vLAPPz9ACJiosARz83AFZNRAB1aV0A1Nj+AFJTVQCzs7MAjX9yAFtdXwDi4uIAqJaGALyplgDRvagAMiwnABkbHQB9g/4ACgoKAOLPuQD+8dcA6+vrAGphVwCdnp4A9t3GAD43LwB0dHQAgYOEADw8PAAdIf8A0tLSAPf5/wC5vv4AxcXGAJ+QgAAbFxMA3eH/AGJjZACtra0A9+rSAE9HQADs7/4AIyQmAHl1aQAsLzEATE5QAEpP/wAhHRoAXFJIAI+PjwAeICIAAwUHAD9BQgBWWFoAwa6bAMm8qAATEA0A///nAOnSuwAVFxgA3d3dAHltYACJe20AlIV2APj4+AA4My4A/vfcAP7s0gDUwa0AuaSSAPviyQA4ODgAt7e3AKGiogAODg8AKyssAMvP/gCBh/4ALiklAIR3awDOzs8A38u2AH9wYwAIBgQAb2NYADYwKgD7+/sA8PDwAPPbxABJS00AwcLCABke/gDY2/8ADgwJAAUDAgAjIB0A+eXMAF9gYgCbm5sAkpOUABQSEQBEPDUAUVFRAIWFhQAZGRkAIyMjAPP1/gDp7P4AtaORAAYHBwAfGxgA39/gAC8xMwDv2cIASUE4AK+vsADmz7kAAQMFABAQEAD+6dAAMSokAEE4MQBVS0EAyLWhAGVlZQCai30AjIyMAP7+4wAUFBQA/vneAP7z2gD+7tUALy8vADs1LgComYgA/f39APb29gAbHR8A/ePLAPngyADb29sAQkRGAEZISgDWw68A07+qAFpQRgBkW1EAvquYAHZ2dgCWh3gADw0MACAiJAD8584ANTc5ADc5OwDZxrEAV1dXAAMCAgDiy7UAjI2PAAwLCwDb3/4Aw7CcAJ+foACDdmgAoZGCABIUFgAjHxoAJSUlAPbcxADIzf4A79fAAOzZwgC3vP4A59G7AOLNtwBdXV0Av62aAAQEBAAGBQQACQgHABYWFgD09PUAFxocADAzNQDDw8MAWE5FAMOynwBaXF0AbmJWAGBiYwC3opAAAgEBABISEgD+6M8A/OXNADAqJgAyLSkANi8oADQ1NgDTvakAtba3AMGwnQBkWk8Aq6ytAG1jWQBjZWYAnY+AAP//5QD+9dsA+N7GAODNtwDRv6oAw66bAAcICQD+/v4A+vr6ABQRDgD5+fkAFhgZAP/43QAaGhoA8fHxAP/w1gD/7dMA6urqAP///wAAVvz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8VgD8NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTU1NTX8AKA1NTU1NTU1NagM/fz8oKCg/KDfqDU1NTU1NTU1NaAAoDU1NTU1NQz8p63CWq4gIJciA5g1oJQ1NTU1NTU1oACgNTU1Naf8uCKLz8NJq9knStX4z/ZNEmDfNTU1NTWgAPw1NTX9Gq/dRzhtXQEBAQEBAaQOF7cAgmH8NTU1NfwAoDU1oNhVtJD09PT09PSlHqT09PT09B1RAOwx7zU1oACgNaDOAEL19PT09CWDhtKd+mSbXfT09PROACb8NTWgAKDgy3sK9PT09PTNALl/D9ZDsdLF+/T09PRmAM5gNaAA/PyuRwEBAQEBAU4dAQEBAQEBdIR0AQEBAQFSxLWo/ADuV93p9PT09PT09PT09PT09PT09PT09PT09IwA8pSgABgqijL09PT09PT09PT09PT09PT09PT09PT09EccoKAAnLBn0/T09PT09COH9PT09PT09GlG9PT09PT0jRxfoADtcQBZAQEBATwICwJBAQEBATxLCzp5AQEBAQFY48n5AIlQkzf09PT0BwsLC2r09PT0xwsLCy709PT0Zc9HLJ8AlhVlADv09PQQGQsLyvT09PSIeAsLPfT09PfSmR5Q3ACVvPSDZ6n09PQjLhb09PT09PS+BD309PTTOYX79NXBAG/UdAG53g0BAQEBAQEBAQEBAQEBAQEBPmihdAH+kmIARGsT23YAALaQXfT09PT09PT09PT0PiTdAKrmNC1y7gD5dVuC186sIdDDJIAPWf4yjB3Afk/doufxIHpV6I5fAKBjoKCglN+gzLKPQNGK82cv0FU2wa1glN+UyHWgY6AA/DU1NTU1NTWnoKBjkSspAD8wdf2gDDU1DJQMqDU1/ACgNTU1NTU1NTU1NainlPx8bPmoNTU1Y5Tlv3XfxjWgAKA1NTU1NTU1NTU1NTU1MeprlDU1NajgEbqKTGHfNaAAoDU1NTU1NTU1NTU1NTWUsLq7pzVjnlySdygFCX2ooAD8NTU1NTU1NTU1NTU1NWN1vcLt/Y5UxOsBAQ4ABgz8AKA1NTU1NTU1NTU1NTU1NfxF4kheky+z5HP0H3DIY6AAoDU1NTU1NTU1NTU1NTU1YBuB4TOj8G6LphQAmmA1oACgNTU1NTU1NTU1NTU1NTU1qMiU/GCnlMvB2lNgNTWgAPw1NTU1NTU1NTU1NTU1NTXvY+81NTU1p6D8lDU1NfwAVvz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8/Pz8VgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAP////8K';
7915 // remove current favicons and replace accordingly, or tinycon has a cross domain issue since the real favicon is on redditstatic.com.
7916 $('head link[rel="shortcut icon"], head link[rel="icon"]').attr('href',faviconDataurl);
7918 if ((modules['betteReddit'].options.showLastEditedTimestamp.value) && ((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments'))) {
7919 RESUtils.addCSS('.edited-timestamp[title]:after{content:" (" attr(title) ")";font-size: 90%;}');
7921 if ((modules['betteReddit'].options.restoreSavedTab.value) && (RESUtils.loggedInUser() !== null) && document.querySelector('.with-listing-chooser:not(.profile-page)')) {
7922 this.restoreSavedTab();
7924 if ((modules['betteReddit'].options.toolbarFix.value) && (RESUtils.pageType() === 'linklist')) {
7925 RESUtils.watchForElement('siteTable', modules['betteReddit'].toolbarFix);
7927 if ((RESUtils.pageType() === 'inbox') && (modules['betteReddit'].options.fullCommentsLink.value)) {
7928 RESUtils.watchForElement('siteTable', modules['betteReddit'].fullComments);
7930 if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (modules['betteReddit'].options.fixSaveLinks.value)) {
7931 RESUtils.watchForElement('siteTable', modules['betteReddit'].fixSaveLinks);
7933 if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (modules['betteReddit'].options.fixHideLinks.value)) {
7934 RESUtils.watchForElement('siteTable', modules['betteReddit'].fixHideLinks);
7936 if ((RESUtils.pageType() === 'comments') && (modules['betteReddit'].options.commentsLinksNewTabs.value)) {
7937 RESUtils.watchForElement('newComments', modules['betteReddit'].commentsLinksNewTabs);
7940 if (this.options.searchSubredditByDefault.value) {
7941 // make sure we're not on a search results page...
7942 if (!location.href.match('/[r|m]/[\\w+\\-]+/search')) {
7943 this.searchSubredditByDefault();
7946 if ((this.options.videoTimes.value) && ((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments'))) {
7947 this.getVideoTimes();
7948 // listen for new DOM nodes so that modules like autopager, river of reddit, etc still get l+c links...
7950 RESUtils.watchForElement('siteTable', modules['betteReddit'].getVideoTimes);
7952 if ((RESUtils.loggedInUser() !== null) && ((this.options.showUnreadCount.value) || (this.options.showUnreadCountInTitle.value) || (this.options.showUnreadCountInFavicon.value))) {
7953 // Reddit CSS change broke this when they went to sprite sheets.. new CSS will fix the issue.
7954 // RESUtils.addCSS('#mail { min-width: 16px !important; width: auto !important; text-indent: 18px !important; background-repeat: no-repeat !important; line-height: 8px !important; }');
7955 // removing text indent - on 11/14/11 reddit changed the mail sprites, so I have to change how this is handled..
7956 RESUtils.addCSS('#mail { top: 2px; min-width: 16px !important; width: auto !important; background-repeat: no-repeat !important; line-height: 8px !important; }');
7957 // RESUtils.addCSS('#mail.havemail { top: 2px !important; margin-right: 1px; }');
7958 RESUtils.addCSS('#mail.havemail { top: 2px !important; }');
7959 if ((BrowserDetect.isChrome()) || (BrowserDetect.isSafari())) {
7960 // I hate that I have this conditional CSS in here but I can't figure out why it's needed for webkit and screws up firefox.
7961 RESUtils.addCSS('#mail.havemail { top: 0; }');
7963 this.showUnreadCount();
7965 switch(this.options.pinHeader.value) {
7968 $('body').addClass('pinHeader-header');
7971 this.pinSubredditBar();
7972 $('body').addClass('pinHeader-sub');
7975 this.pinSubredditBar();
7977 $('body').addClass('pinHeader-subanduser');
7981 $('body').addClass('pinHeader-userbar');
7988 commentsLinksNewTabs: function(ele) {
7989 ele = ele || document.body;
7990 var links = ele.querySelectorAll('div.md a');
7991 for (var i=0, len=links.length; i<len; i++) {
7992 links[i].target = '_blank';
7995 setUpTurboSelfText: function() {
7996 // TODO: Turbo selftext seems a little wonky on NER pages
7997 modules['betteReddit'].selfTextHash = {};
7998 $('body').on('click', '.expando-button.selftext:not(".twitter"):not(.toggleImage)', modules['betteReddit'].showSelfText);
7999 $('#siteTable').data('jsonURL', location.href+'.json');
8001 RESUtils.watchForElement('siteTable', modules['betteReddit'].setNextSelftextURL);
8003 setNextSelftextURL: function(ele) {
8004 if (modules['neverEndingReddit'].nextPageURL) {
8005 var jsonURL = modules['neverEndingReddit'].nextPageURL.replace('/?','/.json?');
8006 $(ele).data('jsonURL',jsonURL);
8009 showSelfText: function(event) {
8010 var thisID = $(event.target).parent().parent().attr('data-fullname');
8011 if (typeof modules['betteReddit'].selfTextHash[thisID] === 'undefined') {
8012 // we haven't gotten JSON data for this set of links yet... get it, then replace the click listeners with our own...
8013 var jsonURL = $(event.target).closest('.sitetable.linklisting').data('jsonURL');
8014 modules['betteReddit'].getSelfTextData(jsonURL);
8016 if ($(event.target).hasClass('collapsed') || $(event.target).hasClass('collapsedExpando')) {
8017 // the duplicate classes here unfortunately have to exist due to Reddit clobbering things with .collapsed
8018 // and no real elegant way that I've thought of to fix the fact that selfText expandos still have that class.
8019 $(event.target).removeClass('collapsed collapsedExpando');
8020 $(event.target).addClass('expanded');
8021 $(event.target).parent().find('.expando').html(
8022 '<form class="usertext"><div class="usertext-body">' +
8023 $('<div/>').html(modules['betteReddit'].selfTextHash[thisID]).text() +
8027 $(event.target).removeClass('expanded');
8028 $(event.target).addClass('collapsedExpando');
8029 $(event.target).addClass('collapsed');
8030 $(event.target).parent().find('.expando').hide();
8035 getSelfTextData: function(href) {
8036 if (!modules['betteReddit'].gettingSelfTextData) {
8037 modules['betteReddit'].gettingSelfTextData = true;
8038 $.getJSON(href, modules['betteReddit'].applyTurboSelfText);
8041 applyTurboSelfText: function(data) {
8042 var linkList = data.data.children;
8044 delete modules['betteReddit'].gettingSelfTextData;
8045 for (var i=0, len=linkList.length; i<len; i++) {
8046 var thisID = linkList[i].data.name;
8048 var thisSiteTable = $('.id-'+thisID).closest('.sitetable.linklisting');
8049 $(thisSiteTable).find('.expando-button.selftext').removeAttr('onclick');
8051 modules['betteReddit'].selfTextHash[thisID] = linkList[i].data.selftext_html;
8054 getInboxLink: function (havemail) {
8055 if (havemail && !modules['betteReddit'].options.unreadLinksToInbox.value) {
8056 return '/message/unread/';
8059 return '/message/inbox/';
8061 showUnreadCount: function() {
8062 if (typeof this.mail === 'undefined') {
8063 this.mail = document.querySelector('#mail');
8065 this.mailCount = createElementWithID('a','mailCount');
8066 this.mailCount.display = 'none';
8067 this.mailCount.setAttribute('href', this.getInboxLink(true));
8068 insertAfter(this.mail, this.mailCount);
8072 $(modules['betteReddit'].mail).html('');
8073 if (this.mail.classList.contains('havemail')) {
8074 this.mail.setAttribute('href', this.getInboxLink(true));
8075 var lastCheck = parseInt(RESStorage.getItem('RESmodules.betteReddit.msgCount.lastCheck.'+RESUtils.loggedInUser()), 10) || 0;
8076 var now = new Date();
8077 // 300000 = 5 minutes... we don't want to annoy Reddit's servers too much with this query...
8078 if ((now.getTime() - lastCheck) > 300000) {
8081 url: location.protocol + '//' + location.hostname + "/message/unread/.json?mark=false&app=res",
8082 onload: function(response) {
8083 // save that we've checked in the last 5 minutes
8084 var now = new Date();
8085 RESStorage.setItem('RESmodules.betteReddit.msgCount.lastCheck.'+RESUtils.loggedInUser(), now.getTime());
8086 var data = JSON.parse(response.responseText);
8087 var count = data.data.children.length;
8088 RESStorage.setItem('RESmodules.betteReddit.msgCount.'+RESUtils.loggedInUser(), count);
8089 modules['betteReddit'].setUnreadCount(count);
8093 var count = RESStorage.getItem('RESmodules.betteReddit.msgCount.'+RESUtils.loggedInUser());
8094 modules['betteReddit'].setUnreadCount(count);
8097 // console.log('no need to get count - no new mail. resetting lastCheck');
8098 modules['betteReddit'].setUnreadCount(0);
8099 RESStorage.setItem('RESmodules.betteReddit.msgCount.lastCheck.'+RESUtils.loggedInUser(), 0);
8103 setUnreadCount: function(count) {
8104 if (this.options.showUnreadCountInFavicon.value) {
8105 //window.Tinycon.setOptions({ fallback: false });
8108 if (this.options.showUnreadCountInTitle.value) {
8109 var newTitle = '[' + count + '] ' + document.title.replace(/^\[[\d]+\]\s/,'');
8110 document.title = newTitle;
8112 if (this.options.showUnreadCountInFavicon.value) {
8113 //window.Tinycon.setBubble(count);
8115 if (this.options.showUnreadCount.value) {
8116 modules['betteReddit'].mailCount.display = 'inline-block'
8117 modules['betteReddit'].mailCount.textContent = '['+count+']';
8118 if (modules['neverEndingReddit'].NREMailCount) {
8119 modules['neverEndingReddit'].NREMailCount.display = 'inline-block'
8120 modules['neverEndingReddit'].NREMailCount.textContent = '['+count+']';
8124 var newTitle = document.title.replace(/^\[[\d]+\]\s/,'');
8125 document.title = newTitle;
8126 if (modules['betteReddit'].mailCount) {
8127 modules['betteReddit'].mailCount.display = 'none';
8128 $(modules['betteReddit'].mailCount).html('');
8129 if (modules['neverEndingReddit'].NREMailCount) {
8130 modules['neverEndingReddit'].NREMailCount.display = 'none'
8131 $(modules['neverEndingReddit'].NREMailCount).html('');
8134 if (this.options.showUnreadCountInFavicon.value) {
8135 //window.Tinycon.setBubble(0);
8152 checkToolbarLink: function(url) {
8153 for (var i=0, len=this.toolbarFixLinks.length; i<len; i++) {
8154 if (url.indexOf(this.toolbarFixLinks[i]) !== -1) return true;
8158 toolbarFix: function(ele) {
8159 var root = ele || document;
8160 var links = root.querySelectorAll('div.entry a.title');
8161 for (var i=0, len=links.length; i<len; i++) {
8162 if (modules['betteReddit'].checkToolbarLink(links[i].getAttribute('href'))) {
8163 links[i].removeAttribute('onmousedown');
8165 // patch below for comments pages thanks to redditor and resident helperninja gavin19
8166 if (links[i].getAttribute('srcurl')) {
8167 if (modules['betteReddit'].checkToolbarLink(links[i].getAttribute('srcurl'))) {
8168 links[i].removeAttribute('onmousedown');
8173 fullComments: function(ele) {
8174 var root = ele || document;
8175 var entries = root.querySelectorAll('#siteTable .entry');
8177 for (var i=0, len=entries.length; i<len;i++) {
8178 var linkEle = entries[i].querySelector('A.bylink');
8179 var thisCommentsLink = '';
8180 if ((typeof linkEle !== 'undefined') && (linkEle !== null)) {
8181 thisCommentsLink = linkEle.getAttribute('href');
8183 if (thisCommentsLink !== '') {
8184 thisCommentsSplit = thisCommentsLink.split("/");
8185 thisCommentsSplit.pop();
8186 thisCommentsLink = thisCommentsSplit.join("/");
8187 linkList = entries[i].querySelector('.flat-list');
8188 var fullCommentsLink = document.createElement('li');
8189 $(fullCommentsLink).html('<a class="redditFullComments" href="' + escapeHTML(thisCommentsLink) + '">'+ escapeHTML(this.options.fullCommentsText.value) +'</a>');
8190 linkList.appendChild(fullCommentsLink);
8194 editMyComments: function(ele) {
8195 var root = ele || document;
8196 var entries = root.querySelectorAll('#siteTable .entry');
8197 for (var i=0, len=entries.length; i<len;i++) {
8198 var linkEle = entries[i].querySelector('A.bylink');
8199 var thisCommentsLink = '';
8200 if ((typeof linkEle !== 'undefined') && (linkEle !== null)) {
8201 thisCommentsLink = linkEle.getAttribute('href');
8203 if (thisCommentsLink !== '') {
8204 permalink = entries[i].querySelector('.flat-list li.first');
8205 var editLink = document.createElement('li');
8206 $(editLink).html('<a onclick="return edit_usertext(this)" href="javascript:void(0);">edit</a>');
8207 insertAfter(permalink, editLink);
8211 fixSaveLinks: function(ele) {
8212 var root = ele || document;
8213 var saveLinks = root.querySelectorAll('li:not(.comment-save-button) > FORM.save-button > SPAN > A');
8214 for (var i=0, len=saveLinks.length; i<len; i++) {
8215 saveLinks[i].removeAttribute('onclick');
8216 saveLinks[i].setAttribute('action','save');
8217 saveLinks[i].addEventListener('click', modules['betteReddit'].saveLink, false);
8219 var unsaveLinks = document.querySelectorAll('FORM.unsave-button > SPAN > A');
8220 for (var i=0, len=saveLinks.length; i<len; i++) {
8221 if (typeof unsaveLinks[i] !== 'undefined') {
8222 unsaveLinks[i].removeAttribute('onclick');
8223 unsaveLinks[i].setAttribute('action','unsave');
8224 unsaveLinks[i].addEventListener('click', modules['betteReddit'].saveLink, false);
8228 fixHideLinks: function(ele) {
8229 var root = ele || document;
8230 var hideLinks = root.querySelectorAll('FORM.hide-button > SPAN > A');
8231 for (var i=0, len=hideLinks.length; i<len; i++) {
8232 hideLinks[i].removeAttribute('onclick');
8233 hideLinks[i].setAttribute('action','hide');
8234 hideLinks[i].addEventListener('click', modules['betteReddit'].hideLinkEventHandler, false);
8236 var unhideLinks = document.querySelectorAll('FORM.unhide-button > SPAN > A');
8237 for (var i=0, len=hideLinks.length; i<len; i++) {
8238 if (typeof unhideLinks[i] !== 'undefined') {
8239 unhideLinks[i].removeAttribute('onclick');
8240 unhideLinks[i].setAttribute('action','unhide');
8241 unhideLinks[i].addEventListener('click', modules['betteReddit'].hideLinkEventHandler, false);
8245 saveLink: function(e) {
8246 if (e) modules['betteReddit'].saveLinkClicked = e.target;
8247 if (modules['betteReddit'].saveLinkClicked.getAttribute('action') === 'unsave') {
8248 $(modules['betteReddit'].saveLinkClicked).text('unsaving...');
8250 $(modules['betteReddit'].saveLinkClicked).text('saving...');
8252 if (modules['betteReddit'].modhash) {
8253 var action = modules['betteReddit'].saveLinkClicked.getAttribute('action');
8254 var parentThing = modules['betteReddit'].saveLinkClicked.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
8255 var idRe = /id-([\w]+)/i;
8256 var getLinkid = idRe.exec(parentThing.getAttribute('class'));
8257 var linkid = getLinkid[1];
8258 if (action === 'unsave') {
8259 var executed = 'unsaved';
8260 var apiURL = location.protocol + '//'+location.hostname+'/api/unsave';
8262 var executed = 'saved';
8263 var apiURL = location.protocol + '//'+location.hostname+'/api/save';
8265 var params = 'id='+linkid+'&executed='+executed+'&uh='+modules['betteReddit'].modhash+'&renderstyle=html';
8271 "Content-Type": "application/x-www-form-urlencoded"
8273 onload: function(response) {
8274 if (response.status === 200) {
8275 if (modules['betteReddit'].saveLinkClicked.getAttribute('action') === 'unsave') {
8276 $(modules['betteReddit'].saveLinkClicked).text('save');
8277 modules['betteReddit'].saveLinkClicked.setAttribute('action','save');
8279 $(modules['betteReddit'].saveLinkClicked).text('unsave');
8280 modules['betteReddit'].saveLinkClicked.setAttribute('action','unsave');
8283 delete modules['betteReddit'].modhash;
8284 alert('Sorry, there was an error trying to '+modules['betteReddit'].saveLinkClicked.getAttribute('action')+' your submission. Try clicking again.');
8291 url: location.protocol + '//'+location.hostname+'/api/me.json?app=res',
8292 onload: function(response) {
8293 var data = safeJSON.parse(response.responseText);
8294 if (typeof data.data === 'undefined') {
8295 alert('Sorry, there was an error trying to '+modules['betteReddit'].saveLinkClicked.getAttribute('action')+' your submission. You may have third party cookies disabled. You will need to either enable third party cookies, or add an exception for *.reddit.com');
8296 } else if ((typeof data.data.modhash !== 'undefined') && (data.data.modhash)) {
8297 modules['betteReddit'].modhash = data.data.modhash;
8298 modules['betteReddit'].saveLink();
8304 hideLinkEventHandler: function(e) {
8305 modules['betteReddit'].hideLink(e.target);
8307 hideLink: function(clickedLink) {
8308 if (clickedLink.getAttribute('action') === 'unhide') {
8309 $(clickedLink).text('unhiding...');
8311 $(clickedLink).text('hiding...');
8313 if (modules['betteReddit'].modhash) {
8314 var action = clickedLink.getAttribute('action');
8315 var parentThing = clickedLink.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
8316 var idRe = /id-([\w]+)/i;
8317 var getLinkid = idRe.exec(parentThing.getAttribute('class'));
8318 var linkid = getLinkid[1];
8319 if (action === 'unhide') {
8320 var executed = 'unhidden';
8321 var apiURL = 'http://'+location.hostname+'/api/unhide';
8323 var executed = 'hidden';
8324 var apiURL = 'http://'+location.hostname+'/api/hide';
8326 var params = 'id='+linkid+'&executed='+executed+'&uh='+modules['betteReddit'].modhash+'&renderstyle=html';
8333 "Content-Type": "application/x-www-form-urlencoded"
8335 onload: function(response) {
8336 if (response.status === 200) {
8337 if (clickedLink.getAttribute('action') === 'unhide') {
8338 $(clickedLink).text('hide');
8339 clickedLink.setAttribute('action','hide');
8340 if (typeof modules['betteReddit'].hideTimer !== 'undefined') clearTimeout(modules['betteReddit'].hideTimer);
8342 $(clickedLink).text('unhide');
8343 clickedLink.setAttribute('action','unhide');
8344 modules['betteReddit'].hideTimer = setTimeout(function() {
8345 modules['betteReddit'].hideFader(clickedLink);
8349 delete modules['betteReddit'].modhash;
8350 alert('Sorry, there was an error trying to '+clickedLink.getAttribute('action')+' your submission. Try clicking again.');
8357 url: location.protocol + '//'+location.hostname+'/api/me.json?app=res',
8358 onload: function(response) {
8359 var data = safeJSON.parse(response.responseText);
8360 if (typeof data.data === 'undefined') {
8361 alert('Sorry, there was an error trying to '+clickedLink.getAttribute('action')+' your submission. You may have third party cookies disabled. You will need to either enable third party cookies, or add an exception for *.reddit.com');
8362 } else if ((typeof data.data.modhash !== 'undefined') && (data.data.modhash)) {
8363 modules['betteReddit'].modhash = data.data.modhash;
8364 modules['betteReddit'].hideLink(clickedLink);
8370 hideFader: function(ele) {
8371 var parentThing = ele.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
8372 RESUtils.fadeElementOut(parentThing, 0.3);
8374 searchSubredditByDefault: function() {
8375 // Reddit now has this feature... but for some reason the box isn't checked by default, so we'll still do that...
8376 var restrictSearch = document.body.querySelector('INPUT[name=restrict_sr]');
8377 if (restrictSearch) {
8378 restrictSearch.checked = true;
8381 getVideoTimes: function(obj) {
8382 obj = obj || document;
8383 var youtubeLinks = obj.querySelectorAll('a.title[href*="youtube.com"], a.title[href*="youtu.be"]');
8384 var shortenedYoutubeLinks = obj.querySelectorAll('a.title[href*="youtube.com"]');
8385 var titleHasTimeRegex = /[\[|\(][0-9]*:[0-9]*[\]|\)]/;
8388 for (var i=0, len=youtubeLinks.length; i<len; i+=1) {
8389 if(!youtubeLinks[i].innerHTML.match(titleHasTimeRegex)) {
8390 ytLinks.push(youtubeLinks[i]);
8393 youtubeLinks = ytLinks;
8394 var getYoutubeIDRegex = /\/?[\&|\?]?v\/?=?([\w\-]{11})&?/i;
8395 var getShortenedYoutubeIDRegex = /([\w\-]{11})&?/i;
8396 var getYoutubeStartTimeRegex = /\[[\d]+:[\d]+\]/i;
8398 modules['betteReddit'].youtubeLinkIDs = {};
8399 modules['betteReddit'].youtubeLinkRefs = [];
8400 for (var i = 0, len = youtubeLinks.length; i < len; i++) {
8401 var match = getYoutubeIDRegex.exec(youtubeLinks[i].getAttribute('href'));
8402 var shortened = /youtu\.be/i;
8403 var isShortened = shortened.exec(youtubeLinks[i].getAttribute('href'));
8405 var smatch = getShortenedYoutubeIDRegex.exec(youtubeLinks[i].getAttribute('href'));
8407 var thisYTID = '"' + smatch[1] + '"';
8408 modules['betteReddit'].youtubeLinkIDs[thisYTID] = youtubeLinks[i];
8409 modules['betteReddit'].youtubeLinkRefs.push([thisYTID, youtubeLinks[i]]);
8412 // add quotes so URL creation is doable with just a join...
8413 var thisYTID = '"' + match[1] + '"';
8414 modules['betteReddit'].youtubeLinkIDs[thisYTID] = youtubeLinks[i];
8415 modules['betteReddit'].youtubeLinkRefs.push([thisYTID, youtubeLinks[i]]);
8417 var timeMatch = getYoutubeStartTimeRegex.exec(youtubeLinks[i].getAttribute('href'));
8418 var titleMatch = youtubeLinks[i].innerHTML.match(titleHasTimeRegex);
8419 if (timeMatch && !titleMatch) {
8420 youtubeLinks[i].textContent += ' (@' + timeMatch[1] + ')';
8423 for (var id in modules['betteReddit'].youtubeLinkIDs){
8426 modules['betteReddit'].youtubeLinkIDs = tempIDs;
8427 modules['betteReddit'].getVideoJSON();
8430 getVideoJSON: function() {
8431 var thisBatch = modules['betteReddit'].youtubeLinkIDs.splice(0,8);
8432 if (thisBatch.length) {
8433 var thisIDString = thisBatch.join('%7C');
8434 // var jsonURL = 'http://gdata.youtube.com/feeds/api/videos?q='+thisIDString+'&fields=entry(id,media:group(yt:duration))&alt=json';
8435 var jsonURL = 'http://gdata.youtube.com/feeds/api/videos?q='+thisIDString+'&v=2&fields=entry(id,title,media:group(yt:duration,yt:videoid,yt:uploaded))&alt=json';
8439 onload: function(response) {
8440 var data = safeJSON.parse(response.responseText, null, true);
8441 if ((typeof data.feed !== 'undefined') && (typeof data.feed.entry !== 'undefined')) {
8442 for (var i=0, len=data.feed.entry.length; i<len; i++) {
8443 var thisYTID = '"'+data.feed.entry[i]['media$group']['yt$videoid']['$t']+'"';
8444 var thisTotalSecs = data.feed.entry[i]['media$group']['yt$duration']['seconds'];
8445 var thisTitle = data.feed.entry[i]['title']['$t'];
8446 var thisMins = Math.floor(thisTotalSecs/60);
8447 var thisSecs = (thisTotalSecs%60);
8448 if (thisSecs < 10) thisSecs = '0'+thisSecs;
8449 var thisTime = ' - [' + thisMins + ':' + thisSecs + ']';
8450 if(modules['betteReddit'].options.videoUploaded.value){
8451 var thisUploaded = data.feed.entry[i]['media$group']['yt$uploaded']['$t'];
8452 thisUploaded = thisUploaded.match(/[^T]*/);
8453 thisTime += '['+ thisUploaded +']';
8455 for (var j = 0, lenj = modules['betteReddit'].youtubeLinkRefs.length; j < lenj; j += 1){
8456 if (modules['betteReddit'].youtubeLinkRefs[j][0] === thisYTID) {
8457 modules['betteReddit'].youtubeLinkRefs[j][1].textContent += ' ' + thisTime;
8458 modules['betteReddit'].youtubeLinkRefs[j][1].setAttribute('title','YouTube title: '+thisTitle);
8462 // wait a bit, make another request...
8463 setTimeout(modules['betteReddit'].getVideoJSON, 500);
8469 pinSubredditBar: function() {
8470 // Make the subreddit bar at the top of the page a fixed element
8471 // The subreddit manager code changes the document's structure
8472 var sm = modules['subredditManager'].isEnabled();
8474 var sb = document.getElementById('sr-header-area');
8475 if (sb == null) return; // reddit is under heavy load
8476 var header = document.getElementById('header');
8478 // add a dummy <div> inside the header to replace the subreddit bar (for spacing)
8479 var spacer = document.createElement('div');
8480 // null parameter is necessary for FF3.6 compatibility.
8481 spacer.style.paddingTop = window.getComputedStyle(sb, null).paddingTop;
8482 spacer.style.paddingBottom = window.getComputedStyle(sb, null).paddingBottom;
8484 // HACK: for some reason, if the SM is enabled, the SB gets squeezed horizontally,
8485 // and takes up three rows of vertical space (even at low horizontal resolution).
8486 if (sm) spacer.style.height = (parseInt(window.getComputedStyle(sb, null).height, 10) / 3 - 3)+'px';
8487 else spacer.style.height = window.getComputedStyle(sb, null).height;
8489 //window.setTimeout(function(){
8490 // add the spacer; take the subreddit bar out of the header and put it above
8491 header.insertBefore(spacer, sb);
8492 document.body.insertBefore(sb,header);
8495 // RESUtils.addCSS('div#sr-header-area {position: fixed; z-index: 10000 !important; left: 0; right: 0; box-shadow: 0 2px 2px #AAA;}');
8496 // something changed on Reddit on 1/31/2012 that made this header-bottom-left margin break subreddit stylesheets... commenting out seems to fix it?
8497 // and now later on 1/31 they've changed it back and I need to add this line back in...
8498 RESUtils.addCSS('#header-bottom-left { margin-top: 19px; }');
8499 RESUtils.addCSS('div#sr-header-area {position: fixed; z-index: 10000 !important; left: 0; right: 0; }');
8500 this.pinCommonElements(sm);
8502 pinUserBar: function() {
8503 // Make the user bar at the top of the page a fixed element
8504 this.userBarElement = document.getElementById('header-bottom-right');
8505 var thisHeight = $('#header-bottom-right').height();
8506 RESUtils.addCSS('#header-bottom-right:hover { opacity: 1 !important; }');
8507 RESUtils.addCSS('#header-bottom-right { height: '+parseInt(thisHeight+1, 10)+'px; }');
8508 // make the account switcher menu fixed
8509 window.addEventListener('scroll', modules['betteReddit'].handleScroll, false);
8510 this.pinCommonElements();
8512 handleScroll: function(e) {
8513 if (modules['betteReddit'].scrollTimer) clearTimeout(modules['betteReddit'].scrollTimer);
8514 modules['betteReddit'].scrollTimer = setTimeout(modules['betteReddit'].handleScrollAfterTimer, 300);
8516 handleScrollAfterTimer: function(e) {
8517 if (RESUtils.elementInViewport(modules['betteReddit'].userBarElement)) {
8518 modules['betteReddit'].userBarElement.setAttribute('style','');
8519 if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
8520 $(modules['accountSwitcher'].accountMenu).attr('style','position: absolute;');
8522 } else if (modules['betteReddit'].options.pinHeader.value === 'subanduser') {
8523 if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
8524 $(modules['accountSwitcher'].accountMenu).attr('style','position: fixed;');
8526 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;');
8528 if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
8529 $(modules['accountSwitcher'].accountMenu).attr('style','position: fixed;');
8531 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;');
8534 pinHeader: function() {
8535 // Makes the Full header a fixed element
8537 // the subreddit manager code changes the document's structure
8538 var sm = modules['subredditManager'].isEnabled();
8540 var header = document.getElementById('header');
8541 if (header == null) return; // reddit is under heavy load
8543 // add a dummy <div> to the document for spacing
8544 var spacer = document.createElement('div');
8545 spacer.id = 'RESPinnedHeaderSpacer';
8547 // without the next line, the subreddit manager would make the subreddit bar three lines tall and very narrow
8548 RESUtils.addCSS('#sr-header-area {left: 0; right: 0;}');
8549 spacer.style.height = $('#header').outerHeight() + "px";
8551 // insert the spacer
8552 document.body.insertBefore(spacer, header.nextSibling);
8554 // make the header fixed
8555 RESUtils.addCSS('#header, #RESAccountSwitcherDropdown {position:fixed;}');
8556 // RESUtils.addCSS('#header {left: 0; right: 0; box-shadow: 0 2px 2px #AAA;}');
8557 RESUtils.addCSS('#header {left: 0; right: 0; }');
8558 var headerHeight = $('#header').height() + 15;
8559 RESUtils.addCSS('#RESNotifications { top: '+headerHeight+'px } ');
8560 this.pinCommonElements(sm);
8562 // TODO Needs testing
8563 // Sometimes this gets executed before the subreddit logo has finished loading. When that
8564 // happens, the spacer gets created too short, so when the SR logo finally loads, the header
8565 // grows and overlaps the top of the page, potentially obscuring the first link. This checks
8566 // to see if the image is finished loading. If it is, then the spacer's height is set. Otherwise,
8567 // it pauses, then loops.
8568 // added a check that this element exists, so it doesn't error out RES.
8569 if (document.getElementById('header-img') && (!document.getElementById('header-img').complete)) setTimeout(function(){
8570 if (document.getElementById('header-img').complete)
8571 // null parameter is necessary for FF3.6 compatibility.
8572 document.getElementById('RESPinnedHeaderSpacer').style.height = window.getComputedStyle(document.getElementById('header'), null).height;
8573 else setTimeout(arguments.callee, 10);
8576 pinCommonElements: function(sm) {
8577 // pin the elements common to both pinHeader() and pinSubredditBar()
8579 // RES's subreddit menu
8580 RESUtils.addCSS('#RESSubredditGroupDropdown, #srList, #RESShortcutsAddFormContainer, #editShortcutDialog {position: fixed !important;}');
8582 RESUtils.addCSS('#sr-more-link: {position: fixed;}');
8585 restoreSavedTab: function() {
8586 var tabmenu = document.querySelector('#header .tabmenu'),
8587 li = document.createElement('li'),
8588 a = document.createElement('a'),
8589 user = RESUtils.loggedInUser();
8590 a.textContent = 'saved';
8591 a.href = '/user/' + user + '/saved/';
8593 tabmenu.appendChild(li);
8597 modules['singleClick'] = {
8598 moduleID: 'singleClick',
8599 moduleName: 'Single Click Opener',
8605 { name: 'open comments then link', value: 'commentsfirst' },
8606 { name: 'open link then comments', value: 'linkfirst' }
8608 value: 'commentsfirst',
8609 description: 'What order to open the link/comments in.'
8614 description: 'Hide the [l=c] when the link is the same as the comments page'
8619 description: 'Open the [l+c] link in background tabs'
8622 description: 'Adds an [l+c] link that opens a link and the comments page in new tabs for you in one click.',
8623 isEnabled: function() {
8624 return RESConsole.getModulePrefs(this.moduleID);
8626 isMatchURL: function() {
8627 return RESUtils.isMatchURL(this.moduleID);
8630 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i,
8631 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\._]*\//i
8634 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\._\/\?]*\/comments[-\w\._\/\?=]*/i
8636 beforeLoad: function() {
8637 if ((this.isEnabled()) && (this.isMatchURL())) {
8638 RESUtils.addCSS('.redditSingleClick { color: #888; font-weight: bold; cursor: pointer; padding: 0 1px; }');
8642 if ((this.isEnabled()) && (this.isMatchURL())) {
8645 // listen for new DOM nodes so that modules like autopager, river of reddit, etc still get l+c links...
8646 RESUtils.watchForElement('siteTable', modules['singleClick'].applyLinks);
8649 applyLinks: function(ele) {
8650 var ele = ele || document;
8651 var entries = ele.querySelectorAll('#siteTable .entry, #siteTable_organic .entry');
8652 for (var i=0, len=entries.length; i<len;i++) {
8653 if ((typeof entries[i] !== 'undefined') && (!entries[i].classList.contains('lcTagged'))) {
8654 entries[i].classList.add('lcTagged')
8655 var thisLA = entries[i].querySelector('A.title');
8656 if (thisLA !== null) {
8657 var thisLink = thisLA.getAttribute('href');
8658 var thisComments = entries[i].querySelector('.comments');
8659 if (!(thisLink.match(/^https?/i))) {
8660 thisLink = location.protocol + '//' + document.domain + thisLink;
8662 var thisUL = entries[i].querySelector('ul.flat-list');
8663 var singleClickLI = document.createElement('li');
8664 // changed from a link to a span because you can't cancel a new window on middle click of a link during the mousedown event, and a click event isn't triggered.
8665 var singleClickLink = document.createElement('span');
8666 // singleClickLink.setAttribute('href','javascript:void(0);');
8667 singleClickLink.setAttribute('class','redditSingleClick');
8668 singleClickLink.setAttribute('thisLink',thisLink);
8669 singleClickLink.setAttribute('thisComments',thisComments);
8670 if (thisLink != thisComments) {
8671 singleClickLink.textContent = '[l+c]';
8672 } else if (!(modules['singleClick'].options.hideLEC.value)) {
8673 singleClickLink.textContent = '[l=c]';
8675 singleClickLI.appendChild(singleClickLink);
8676 thisUL.appendChild(singleClickLI);
8677 // we have to switch to mousedown because Webkit is being a douche and not triggering click events on middle click.
8678 // ?? We should still preventDefault on a click though, maybe?
8679 singleClickLink.addEventListener('mousedown', function(e) {
8681 var lcMouseBtn = (modules['singleClick'].options.openBackground.value) ? 1 : 0;
8682 if (e.button !== 2) {
8683 // check if it's a relative link (no http://domain) because chrome barfs on these when creating a new tab...
8684 var thisLink = $(this).parent().parent().parent().find('a.title').attr('href');
8685 if (!(thisLink.match(/^http/i))) {
8686 thisLink = 'http://' + document.domain + thisLink;
8688 if (BrowserDetect.isChrome()) {
8690 requestType: 'singleClick',
8692 openOrder: modules['singleClick'].options.openOrder.value,
8693 commentsURL: this.getAttribute('thisComments'),
8697 chrome.extension.sendMessage(thisJSON);
8698 } else if (BrowserDetect.isSafari()) {
8700 requestType: 'singleClick',
8702 openOrder: modules['singleClick'].options.openOrder.value,
8703 commentsURL: this.getAttribute('thisComments'),
8707 safari.self.tab.dispatchMessage("singleClick", thisJSON);
8708 } else if (BrowserDetect.isOpera()) {
8710 requestType: 'singleClick',
8712 openOrder: modules['singleClick'].options.openOrder.value,
8713 commentsURL: this.getAttribute('thisComments'),
8717 opera.extension.postMessage(JSON.stringify(thisJSON));
8718 } else if (BrowserDetect.isFirefox()) {
8720 requestType: 'singleClick',
8722 openOrder: modules['singleClick'].options.openOrder.value,
8723 commentsURL: this.getAttribute('thisComments'),
8727 self.postMessage(thisJSON);
8729 var thisLink = $(this).parent().parent().parent().find('a.title').attr('href');
8730 if (!(thisLink.match(/^http/i))) {
8731 thisLink = 'http://' + document.domain + thisLink;
8733 if (modules['singleClick'].options.openOrder.value === 'commentsfirst') {
8734 if (thisLink !== this.getAttribute('thisComments')) {
8735 // console.log('open comments');
8736 window.open(this.getAttribute('thisComments'));
8738 window.open(thisLink);
8740 window.open(thisLink);
8741 if (thisLink !== this.getAttribute('thisComments')) {
8742 // console.log('open comments');
8743 window.open(this.getAttribute('thisComments'));
8756 modules['commentPreview'] = {
8757 moduleID: 'commentPreview',
8758 moduleName: 'Live Comment Preview',
8759 category: 'Comments',
8764 description: 'Enable the 2 column editor.'
8767 description: 'Provides a live preview of comments, as well as a two column editor for writing walls of text.',
8768 isEnabled: function() {
8769 return RESConsole.getModulePrefs(this.moduleID);
8772 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/?[-\w\.]*/i,
8773 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i,
8774 /^https?:\/\/([a-z]+)\.reddit\.com\/message\/[-\w\.]*\/?[-\w\.]*/i,
8775 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\.]*\/submit\/?/i,
8776 /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.\/]*\/?/i,
8777 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/about\/edit/i,
8778 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/wiki\/create(\/\w+)?/i,
8779 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/wiki\/edit(\/\w+)?/i,
8780 /^https?:\/\/([a-z]+)\.reddit\.com\/submit\/?/i
8782 isMatchURL: function() {
8783 return RESUtils.isMatchURL(this.moduleID);
8785 beforeLoad: function() {
8786 if ((this.isEnabled()) && (this.isMatchURL())) {
8787 RESUtils.addCSS('.RESDialogSmall.livePreview { position: relative; width: auto; margin-bottom: 15px; }');
8788 RESUtils.addCSS('.RESDialogSmall.livePreview .RESDialogContents h3 { font-weight: bold; }');
8792 if ((this.isEnabled()) && (this.isMatchURL())) {
8793 this.isWiki = $(document.body).is(".wiki-page");
8795 this.converter = window.SnuOwnd.getParser();
8797 //We need to configure non-default renderers here
8798 var SnuOwnd = window.SnuOwnd;
8799 var redditCallbacks = SnuOwnd.getRedditCallbacks();
8800 var rendererConfig = SnuOwnd.defaultRenderState();
8801 rendererConfig.flags = SnuOwnd.DEFAULT_WIKI_FLAGS;
8802 rendererConfig.html_element_whitelist = SnuOwnd.DEFAULT_HTML_ELEMENT_WHITELIST;
8803 rendererConfig.html_attr_whitelist = SnuOwnd.DEFAULT_HTML_ATTR_WHITELIST;
8804 this.converter = SnuOwnd.getParser({
8805 callbacks: redditCallbacks,
8806 context: rendererConfig
8809 this.tocConverter = SnuOwnd.getParser(SnuOwnd.getTocRenderer());
8812 this.bigTextTarget = null;
8813 if (this.options.enableBigEditor.value) {
8814 // Install the 2 column editor
8815 modules['commentPreview'].addBigEditor();
8817 if (modules['keyboardNav'].isEnabled()) {
8818 $("body").delegate(".usertext-edit textarea, #wiki_page_content", "keydown", function(e) {
8819 if (checkKeysForEvent(e, modules["keyboardNav"].options.openBigEditor.value)) {
8820 modules["commentPreview"].showBigEditor.call(this, e);
8826 //Close the preview on submit
8827 $("body").delegate("form", {
8828 submit: function(e) {
8829 $(this).find(".livePreview").hide();
8834 //Perform initial setup of tools over the whole page}
8835 this.attachPreview();
8836 // Wireup reply editors
8837 RESUtils.watchForElement("newCommentsForms", modules["commentPreview"].attachPreview);
8838 // Wireup edit editors (usertext-edit already exists in the page)
8839 RESUtils.watchForElement("newComments", modules["commentPreview"].attachPreview);
8841 this.attachWikiPreview()
8845 markdownToHTML: function(md) {
8846 var body = this.converter.render(md);
8849 <s>Snudown, and therefore SnuOwnd, is a bit funny about how it generates its table of contents entries.
8850 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.
8852 It would be nicer if they just used different functions for rendering the emphasis when making headers.</s>
8854 It seems that my understanding was wrong, for some reason reddit doesn't even use snudown's TOC renderer.
8856 var doc = $('<body>').html(body);
8857 var header_ids = {};
8858 var headers = doc.find('h1, h2, h3, h4, h5, h6');
8859 var tocDiv = $('<div>').addClass('toc');
8860 var parent = $('<ul>');
8861 parent.data('level', 0);
8862 tocDiv.append(parent);
8863 var level = 0, previous = 0;
8865 headers.each(function(i, e) {
8866 var contents = $(this).text();
8867 var aid = $('<div>').html(contents).text();
8868 aid = prefix + '_' + aid.replace(/ /g, '_').toLowerCase();
8869 aid = aid.replace(/[^\w.-]/g, function(s) {
8870 return '.' + s.charCodeAt(0).toString(16).toUpperCase();
8872 if (!(aid in header_ids)) header_ids[aid] = 0;
8873 var id_num = header_ids[aid] + 1;
8874 header_ids[aid] += 1;
8876 if (id_num > 1) aid = aid + id_num;
8878 $(this).attr('id', aid);
8880 var li = $('<li>').addClass(aid);
8881 var a = $('<a>').attr('href', '#'+aid).text(contents);
8884 var thisLevel = +this.tagName.slice(-1);
8885 if (previous && thisLevel > previous) {
8886 var newUL = $('<ul>');
8887 newUL.data('level', thisLevel);
8888 parent.append(newUL);
8891 } else if (level && thisLevel < previous) {
8892 while (level && parent.data('level') > thisLevel) {
8893 parent = parent.closest('ul');
8897 previous = thisLevel;
8900 doc.prepend(tocDiv);
8906 makeBigEditorButton: function() {
8907 return $('<button class="RESBigEditorPop" tabIndex="3">big editor</button>');
8909 attachPreview: function(usertext) {
8910 if (usertext == null) usertext = document.body;
8911 if (modules['commentPreview'].options.enableBigEditor.value) {
8912 modules['commentPreview'].makeBigEditorButton().prependTo($('.bottom-area:not(:has(.RESBigEditorPop))', usertext));
8914 $(usertext).find(".usertext-edit").each(function() {
8915 var preview = $(this).find(".livePreview");
8916 if (preview.length === 0) {
8917 preview = modules["commentPreview"].makePreviewBox();
8918 $(this).append(preview);
8920 var contents = preview.find(".RESDialogContents");
8921 $(this).find("textarea[name=text], textarea[name=description], textarea[name=public_description]").bind("input", function() {
8922 var textarea = $(this);
8923 RESUtils.debounce('refreshPreview', 250, function() {
8924 var markdownText = textarea.val();
8925 if (markdownText.length > 0) {
8926 var html = modules["commentPreview"].markdownToHTML(markdownText);
8928 contents.html(html);
8937 attachWikiPreview: function() {
8938 if (modules['commentPreview'].options.enableBigEditor.value) {
8939 modules['commentPreview'].makeBigEditorButton().insertAfter($('.pageactions'));
8941 var preview = modules["commentPreview"].makePreviewBox();
8942 preview.find(".md").addClass("wiki");
8943 preview.insertAfter($("#editform > br").first());
8945 var contents = preview.find(".RESDialogContents");
8946 $("#wiki_page_content").bind("input", function() {
8948 var textarea = $(this);
8949 RESUtils.debounce('refreshPreview', 250, function() {
8950 var markdownText = textarea.val();
8951 if (markdownText.length > 0) {
8952 var html = modules["commentPreview"].markdownToHTML(markdownText)
8954 contents.html(html);
8962 makePreviewBox: function() {
8963 return $("<div style=\"display: none\" class=\"RESDialogSmall livePreview\"><h3>Live Preview</h3><div class=\"md RESDialogContents\"></div></div>");
8965 addBigEditor: function() {
8966 var editor = $('<div id="BigEditor">').hide();
8967 var left = $('<div class="BELeft RESDialogSmall"><h3>Editor</h3></div>');
8968 var contents = $('<div class="RESDialogContents"><textarea id="BigText" class=""></textarea></div>');
8969 var foot = $('<div class="BEFoot">');
8970 foot.append($('<button style="float:left;">save</button>').bind('click', function() {
8971 var len = $('#BigText').val().length;
8972 var max = $('#BigText').attr("data-max-length");
8974 $('#BigEditor .errorList .error').hide().filter('.TOO_LONG').text('this is too long (max: '+max+')').show();
8975 } else if (len === 0) {
8976 $('#BigEditor .errorList .error').hide().filter('.NO_TEXT').show();
8977 } else if (modules['commentPreview'].bigTextTarget) {
8978 modules['commentPreview'].bigTextTarget.submit();
8979 modules['commentPreview'].bigTextTarget.parents('.usertext-edit:first').find('.livePreview .md').html('');
8980 modules.commentPreview.hideBigEditor(false, true);
8982 $('#BigEditor .errorList .error').hide().filter('.NO_TARGET').show();
8986 foot.append($('<button style="float:left;">close</button>').bind('click', modules.commentPreview.hideBigEditor));
8988 foot.append($('<span class="errorList">\
8989 <span style="display: none;" class="error NO_TEXT">we need something here</span>\
8990 <span style="display: none;" class="error TOO_LONG">this is too long (max: 10000)</span>\
8991 <span style="display: none;" class="error NO_TARGET">there is no associated textarea</span>\
8994 contents.append(foot);
8995 left.append(contents);
8997 var right = $('<div class="BERight RESDialogSmall"><h3>Preview</h3><div class="RESCloseButton RESFadeButton"></div><div class="RESCloseButton close">X</div>\
8998 <div class="RESDialogContents"><div id="BigPreview" class=" md"></div></div></div>');
8999 editor.append(left).append(right);
9001 $(document.body).append(editor);
9003 $('.BERight .RESCloseButton.close').bind("click", modules.commentPreview.hideBigEditor);
9004 $('.BERight .RESFadeButton').bind({
9005 click: function(e) {
9006 $("#BigEditor").fadeTo(300, 0.3);
9007 $(document.body).removeClass("RESScrollLock");
9008 this.isFaded = true;
9010 mouseout: function(e) {
9011 if (this.isFaded) $("#BigEditor").fadeTo(300, 1.0);
9012 $(document.body).addClass("RESScrollLock");
9013 this.isFaded = false;
9016 $('body').delegate('.RESBigEditorPop', 'click', modules.commentPreview.showBigEditor);
9018 $('#BigText').bind('input', function() {
9019 RESUtils.debounce('refreshBigPreview', 250, function() {
9020 var text = $('#BigText').val();
9021 var html = modules['commentPreview'].markdownToHTML(text);
9022 $('#BigPreview').html(html);
9023 if (modules['commentPreview'].bigTextTarget) {
9024 modules['commentPreview'].bigTextTarget.val(text);
9027 }).bind("keydown", function(e) {
9028 //Close big editor on escape
9029 if (e.keyCode === modules["commentTools"].KEYS.ESCAPE) {
9030 modules["commentPreview"].hideBigEditor();
9035 if (modules['commentTools'].isEnabled()) {
9036 contents.prepend(modules['commentTools'].makeEditBar());
9039 showBigEditor: function(e) {
9041 // modules.commentPreview.bigTextTarget = null;
9042 modules.commentPreview.hideBigEditor(true);
9043 $('.side').addClass('BESideHide');
9044 $('body').addClass('RESScrollLock');
9045 RESUtils.fadeElementIn(document.getElementById('BigEditor'), 0.3);
9047 if (!modules['commentPreview'].isWiki) {
9048 baseText = $(this).parents('.usertext-edit:first').find('textarea');
9049 $("#BigPreview").removeClass("wiki");
9050 $(".BERight .RESDialogContents").removeClass("wiki-page-content");
9052 baseText = $('#wiki_page_content');
9053 $("#BigPreview").addClass("wiki");
9054 $(".BERight .RESDialogContents").addClass("wiki-page-content");
9057 var markdown = baseText.val();
9058 var maxLength = baseText.attr("data-max-length");
9059 $("#BigText").attr("data-max-length", maxLength).val(markdown).focus();
9060 modules['commentTools'].updateCounter($("#BigText")[0]);
9061 $('#BigPreview').html(modules['commentPreview'].markdownToHTML(markdown));
9062 modules.commentPreview.bigTextTarget = baseText;
9064 hideBigEditor: function(quick, submitted) {
9065 if (quick === true) {
9066 $('#BigEditor').hide();
9068 RESUtils.fadeElementOut(document.getElementById('BigEditor'), 0.3);
9070 $('.side').removeClass('BESideHide');
9071 $('body').removeClass('RESScrollLock');
9072 var target = modules['commentPreview'].bigTextTarget;
9074 if (target != null) {
9075 target.val($('#BigText').val())
9077 if (submitted !== true) {
9078 var inputEvent = document.createEvent("HTMLEvents");
9079 inputEvent.initEvent("input", true, true);
9080 target[0].dispatchEvent(inputEvent);
9082 modules['commentPreview'].bigTextTarget = null;
9089 modules['commentTools'] = {
9090 moduleID: 'commentTools',
9091 moduleName: 'Comment Tools',
9092 category: 'Comments',
9097 description: 'Shows your currently logged in username to avoid posting from the wrong account.'
9102 description: 'Show user autocomplete tool when typing in posts, comments and replies'
9104 subredditAutocomplete: {
9107 description: 'Show subreddit autocomplete tool when typing in posts, comments and replies'
9112 description: 'When submitting, display the number of characters entered in the title and text fields and indicate when you go over the 300 character limit for titles.'
9114 keyboardShortcuts: {
9117 description: 'Use keyboard shortcuts to apply styles to selected text'
9121 addRowText: '+add shortcut',
9123 { name: 'label', type: 'text' },
9124 { name: 'text', type: 'textarea' },
9125 { name: 'category', type: 'text' },
9126 { name: 'key', type: 'keycode' }
9130 description: "Add buttons to insert frequently used snippets of text."
9132 keepMacroListOpen: {
9135 description: 'After selecting a macro from the dropdown list, do not hide the list.'
9138 description: 'Provides shortcuts for easier markdown.',
9139 isEnabled: function() {
9140 return RESConsole.getModulePrefs(this.moduleID);
9143 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/?[-\w\.]*/i,
9144 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i,
9145 /^https?:\/\/([a-z]+)\.reddit\.com\/message\/[-\w\.]*\/?[-\w\.]*/i,
9146 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\.]*\/submit\/?/i,
9147 /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.\/]*\/?/i,
9148 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/about\/edit/i,
9149 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/wiki\/create(\/\w+)?/i,
9150 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\-\w\.]+\/wiki\/edit(\/\w+)?/i,
9151 /^https?:\/\/([a-z]+)\.reddit\.com\/submit\/?/i
9153 isMatchURL: function() {
9154 return RESUtils.isMatchURL(this.moduleID);
9156 beforeLoad: function() {
9157 if ((this.isEnabled()) && (this.isMatchURL())) {
9158 RESUtils.addCSS('.markdownEditor { white-space: nowrap; }');
9159 RESUtils.addCSS('.RESMacroWrappingSpan { white-space: normal; }');
9160 RESUtils.addCSS('.markdownEditor a { margin-right: 8px; text-decoration: none; font-size: 11px; }');
9161 RESUtils.addCSS('.markdownEditor .RESMacroDropdown {font-size: 10px; }');
9162 RESUtils.addCSS('.selectedItem { color: #fff; background-color: #5f99cf; }');
9163 // RESUtils.addCSS('.RESDialogSmall.livePreview { position: relative; width: auto; margin-bottom: 15px; }');
9164 // RESUtils.addCSS('.RESDialogSmall.livePreview .RESDialogContents h3 { font-weight: bold; }');
9165 RESUtils.addCSS('.RESMacroDropdownTitle, .RESMacroDropdownTitleOverlay { cursor: pointer; display: inline-block; font-size: 11px; text-decoration: underline; color: gray; padding-left: 2px; padding-right: 21px; background-image: url(http://www.redditstatic.com/droparrowgray.gif); background-position: 100% 50%; background-repeat: no-repeat; }');
9166 RESUtils.addCSS('.RESMacroDropdownTitleOverlay { cursor: pointer; }');
9167 RESUtils.addCSS('.RESMacroDropdownContainer { display: none; position: absolute; }');
9168 RESUtils.addCSS('.RESMacroDropdown { display: none; position: absolute; z-index: 2001; }');
9169 RESUtils.addCSS('.RESMacroDropdownList { margin-top: 0; width: auto; max-width: 300px; }');
9170 RESUtils.addCSS('.RESMacroDropdownList a, .RESMacroDropdownList li { font-size: 10px; }');
9171 RESUtils.addCSS('.RESMacroDropdown li { padding-right: 10px; height: 25px; line-height: 24px; }');
9175 STYLESHEET: 128*1024,
9182 //Moved this out of go() because the large commentPreview may need it.
9183 macroCallbackTable: [],
9186 if ((this.isEnabled()) && (this.isMatchURL())) {
9187 this.isWiki = $(document.body).is(".wiki-page");
9189 $("body").delegate("li.viewSource a", {
9190 click: function(e) {
9192 modules["commentTools"].viewSource(this);
9194 }).delegate(".usertext-edit.viewSource .cancel", {
9196 $(this).parents(".usertext-edit.viewSource").hide();
9199 }).delegate("div.markdownEditor a", {
9200 click: function(e) {
9203 var index = $(this).attr("data-macro-index");
9204 var box = modules["commentTools"].findTextareaForElement(this)[0];
9205 // var box = $(this).closest(".usertext-edit, .RESDialogContents, .wiki-page-content").find("textarea[name=text], textarea[name=description], textarea[name=public_description]")[0];
9207 console.error("Failed to locate textarea.");
9210 var handler = modules["commentTools"].macroCallbackTable[index];
9212 console.error("Failed to locate find callback.");
9215 handler.call(modules["commentTools"], this, box);
9218 //Fire an input event to refresh the preview
9219 var inputEvent = document.createEvent("HTMLEvents");
9220 inputEvent.initEvent("input", true, true);
9221 box.dispatchEvent(inputEvent);
9223 }).delegate(".RESMacroDropdownTitle", {
9224 click: function(e) {
9225 var pos = $(this).position();
9226 $(this).next().css({
9227 top: (pos).top+"px",
9228 left: (pos).left+"px"
9231 }).delegate(".RESMacroDropdown", {
9232 mouseleave: function(e) {
9237 if (this.options.showInputLength.value) {
9239 $("body").delegate(".usertext-edit textarea, #title-field textarea, #BigEditor textarea, #wiki_page_content", {
9241 modules['commentTools'].updateCounter(this);
9246 if (this.options.keyboardShortcuts.value) {
9247 $("body").delegate(".usertext-edit textarea, #BigEditor textarea, #wiki_page_content", {
9248 keydown: function(e) {
9249 if (e.keyCode === modules["commentTools"].KEYS.ESCAPE) {
9250 if (!modules["commentTools"].autoCompletePop.is(':visible')) {
9251 // Blur from the editor on escape, so we can return to using the keyboard nav.
9252 // NOTE: The big editor closes on ESC so this won't be reached in that case.
9261 for (var i = 0; i < modules["commentTools"].macroKeyTable.length; i++) {
9262 var row = modules["commentTools"].macroKeyTable[i];
9263 var testedKeyArray = row[0], macroIndex = row[1];
9264 if (checkKeysForEvent(e, testedKeyArray)) {
9265 var handler = modules["commentTools"].macroCallbackTable[macroIndex];
9266 handler.call(modules["commentTools"], null, this);
9267 //Fire an input event to refresh the preview
9268 var inputEvent = document.createEvent("HTMLEvents");
9269 inputEvent.initEvent("input", true, true);
9270 this.dispatchEvent(inputEvent);
9278 if (this.options.subredditAutocomplete.value || this.options.userAutocomplete.value) {
9279 this.addAutoCompletePop();
9282 //Perform initial setup of tools over the whole page
9283 this.attachCommentTools();
9284 this.attatchViewSourceButtons()
9286 //These are no longer necessary but I am saving them in case Reddit changes how they make their reply forms.
9287 // Wireup reply editors
9288 RESUtils.watchForElement("newCommentsForms", modules["commentTools"].attachCommentTools);
9289 // Wireup edit editors (usertext-edit already exists in the page)
9290 RESUtils.watchForElement("newComments", modules["commentTools"].attachCommentTools);
9292 RESUtils.watchForElement("newComments", modules["commentTools"].attatchViewSourceButtons);
9295 migrateData: function() {
9296 var LATEST_MACRO_DATA_VERSION = "2";
9297 var macroVersion = RESStorage.getItem("RESmodules.commentTools.macroDataVersion");
9298 if (macroVersion == null || macroVersion === "0") {
9299 //In this case it is unmigrated or uncreated
9300 var previewOptionString = RESStorage.getItem("RESoptions.commentPreview");
9301 var previewOptions = safeJSON.parse(previewOptionString, "commentPreview");
9302 if (previewOptions != null) {
9303 if (typeof previewOptions.commentingAs !== "undefined") {
9304 this.options.commentingAs.value = previewOptions.commentingAs.value
9305 delete previewOptions.commentingAs;
9307 if (typeof previewOptions.keyboardShortcuts !== "undefined") {
9308 this.options.keyboardShortcuts.value = previewOptions.keyboardShortcuts.value
9309 delete previewOptions.keyboardShortcuts;
9311 if (typeof previewOptions.subredditAutocomplete !== "undefined") {
9312 this.options.subredditAutocomplete.value = previewOptions.subredditAutocomplete.value
9313 delete previewOptions.subredditAutocomplete;
9315 if (typeof previewOptions.macros !== "undefined") {
9317 macros = this.options.macros.value = previewOptions.macros.value
9318 for (var i = 0; i < macros.length; i++) {
9319 while (macros[i].length < 4) macros[i].push("");
9321 delete previewOptions.macros;
9323 RESStorage.setItem("RESoptions.commentTools", JSON.stringify(this.options));
9324 RESStorage.setItem("RESoptions.commentPreview", JSON.stringify(previewOptions));
9325 RESStorage.setItem("RESmodules.commentTools.macroDataVersion", LATEST_MACRO_DATA_VERSION);
9327 //No migration will be performed
9328 RESStorage.setItem("RESmodules.commentTools.macroDataVersion", LATEST_MACRO_DATA_VERSION);
9330 } if (macroVersion === "1") {
9331 var macros = this.options.macros.value;
9332 for (var i = 0; i < macros.length; i++) {
9333 while (macros[i].length < 4) macros[i].push("");
9335 RESStorage.setItem("RESmodules.commentTools.macroDataVersion", LATEST_MACRO_DATA_VERSION);
9338 attatchViewSourceButtons: function(entry) {
9339 var entries = entry == null ? $(".entry", document.body) : $(entry);
9340 if (RESUtils.pageType() === "comments" || RESUtils.pageType() === "inbox") {
9341 //Disabled syncronous version
9342 // $(".flat-list.buttons", entries).find("li:nth-child(2), li:only-child").after('<li class="viewSource"><a href="javascript:void(0)">source</a></li>');
9343 var menus = $(".flat-list.buttons li:first-child", entries);
9344 RESUtils.forEachChunked(menus, 30, 500, function(item, i, array) {
9345 $(item).after('<li class="viewSource"><a href="javascript:void(0)">source</a></li>');
9349 viewSource: function(button) {
9350 var buttonList = $(button).parent().parent();
9351 var sourceDiv = $(button).closest('.thing').find(".usertext-edit.viewSource:first");
9352 if (sourceDiv.length !== 0) {
9355 var permaLink = buttonList.find(".first a");
9356 var jsonURL = permaLink.attr("href");
9357 var urlSplit = jsonURL.split('/');
9358 var postID = urlSplit[urlSplit.length - 1];
9360 var isSelfText = permaLink.is(".comments");
9361 if (jsonURL.indexOf('?context') !== -1) {
9362 jsonURL = jsonURL.replace('?context=3','.json?');
9364 jsonURL += '/.json';
9366 this.gettingSource = this.gettingSource || {};
9367 if (this.gettingSource[postID]) return;
9368 this.gettingSource[postID] = true;
9373 onload: function(response) {
9374 var thisResponse = JSON.parse(response.responseText);
9375 var userTextForm = $('<div class="usertext-edit viewSource"><div><textarea rows="1" cols="1" name="text"></textarea></div><div class="bottom-area"><div class="usertext-buttons"><button type="button" class="cancel">hide</button></div></div></div>');
9377 var sourceText = null;
9378 if (typeof thisResponse[1] !== 'undefined') {
9379 sourceText = thisResponse[1].data.children[0].data.body;
9381 var thisData = thisResponse.data.children[0].data;
9382 if (thisData.id == postID) {
9383 sourceText = thisData.body;
9385 // The message we want is a reply to a PM/modmail, but reddit returns the whole thread.
9386 // So, we have to dig into the replies to find the message we want.
9387 for (var i=0, len=thisData.replies.data.children.length; i<len; i++) {
9388 var replyData = thisData.replies.data.children[i].data;
9389 if (replyData.id == postID) {
9390 sourceText = replyData.body;
9396 // sourceText in this case is reddit markdown. escaping it would screw it up.
9397 userTextForm.find("textarea[name=text]").html(sourceText);
9399 var sourceText = thisResponse[0].data.children[0].data.selftext;
9400 // console.log(sourceText);
9401 // sourceText in this case is reddit markdown. escaping it would screw it up.
9402 userTextForm.find("textarea[name=text]").html(sourceText);
9404 buttonList.before(userTextForm);
9409 attachCommentTools: function (elem) {
9410 if (elem == null) elem = document.body;
9411 $(elem).find("textarea[name]").each(modules["commentTools"].attachEditorToUsertext);
9413 attachEditorToUsertext: function() {
9414 if (this.hasAttribute("data-max-length")) return;
9416 switch (this.name) {
9417 case "title": limit = modules['commentTools'].SUBMIT_LIMITS.POST_TITLE; break;
9418 case "text": limit = modules['commentTools'].SUBMIT_LIMITS.POST; break;
9419 case "description": limit = modules['commentTools'].SUBMIT_LIMITS.SIDEBAR; break;
9420 case "public_description": limit = modules['commentTools'].SUBMIT_LIMITS.DESCRIPTION; break;
9421 case "content": limit = modules['commentTools'].SUBMIT_LIMITS.WIKI; break;
9422 case "description_conflict_old": return;
9423 case "public_description_conflict_old": return;
9425 // console.warn("unhandled form", this);
9428 $(this).attr("data-max-length", limit);
9431 if (this.name === "title") return;
9433 var bar = modules['commentTools'].makeEditBar();
9434 if (this.id === "wiki_page_content") {
9435 $(this).parent().prepend(bar);
9437 $(this).parent().before(bar);
9439 modules['commentTools'].updateCounter(this);
9441 updateCounter: function(textarea) {
9442 var length = $(textarea).val().length;
9443 var limit = +$(textarea).attr("data-max-length");
9444 var counter = $(textarea).parent().parent().find(".markdownEditor .RESCharCounter");
9445 counter.attr('title', 'character limit: '+length+'/'+limit);
9446 counter.text(length+'/'+limit);
9447 if (length > limit) {
9448 counter.addClass('tooLong');
9450 counter.removeClass('tooLong');
9453 makeEditBar: function() {
9454 if (this.cachedEditBar != null) {
9455 return $(this.cachedEditBar).clone();
9458 var editBar = $('<div class="markdownEditor">');
9460 editBar.append(this.makeEditButton("<b>Bold</b>", "ctrl-b", [66, false, true, false, false], function(button, box) {
9461 this.wrapSelection(box, "**", "**");
9463 editBar.append(this.makeEditButton("<i>Italic</i>", "ctrl-i", [73, false, true, false, false], function(button, box) {
9464 this.wrapSelection(box, "*", "*");
9466 editBar.append(this.makeEditButton("<del>strike</del>", "ctrl-s", 83, function(button, box) {
9467 this.wrapSelection(box, "~~", "~~");
9469 editBar.append(this.makeEditButton("<sup>sup</sup>", "", null, function(button, box) {
9470 this.wrapSelectedWords(box, "^");
9472 editBar.append(this.makeEditButton("Link", "", null, function(button, box) {
9473 this.linkSelection(box);
9475 editBar.append(this.makeEditButton(">Quote", "", null, function(button, box) {
9476 this.wrapSelectedLines(box, "> ", "");
9478 editBar.append(this.makeEditButton("<span style=\"font-family: Courier New\">Code</span>", "", null, function(button, box) {
9479 this.wrapSelectedLines(box, " ", "");
9481 editBar.append(this.makeEditButton("•Bullets", "", null, function(button, box) {
9482 this.wrapSelectedLines(box, "* ", "");
9484 editBar.append(this.makeEditButton("1.Numbers", "", null, function(button, box) {
9485 this.wrapSelectedLines(box, "1. ", "");
9488 if (modules["commentTools"].options.showInputLength.value) {
9489 var counter = $('<span class="RESCharCounter" title="character limit: 0/?????">0/?????</span>');
9490 editBar.append(counter);
9493 this.addButtonToMacroGroup("", this.makeEditButton("reddiquette", "", null, function(button, box){
9494 var clickCount = $(button).data("clickCount") || 0;
9496 $(button).data("clickCount", clickCount);
9497 if (clickCount > 2) {
9500 this.macroSelection(box, "[reddiquette](http://www.reddit.com/help/reddiquette) ", "");
9503 this.addButtonToMacroGroup("", this.makeEditButton("[Promote]", "", null, function(button, box){
9504 var clickCount = $(button).data("clickCount") || 0;
9506 $(button).data("clickCount", clickCount);
9507 if (clickCount > 2) {
9509 modules["commentTools"].lod();
9511 this.macroSelection(box, "[Reddit Enhancement Suite](http://redditenhancementsuite.com) ");
9514 this.addButtonToMacroGroup("", this.makeEditButton("ಠ\_ಠ", "Look of disaproval", null, function(button, box) {
9515 this.macroSelection(box, "ಠ\_ಠ");
9517 this.buildMacroDropdowns(editBar);
9518 var addMacroButton = modules['commentTools'].makeEditButton(modules['commentTools'].options.macros.addRowText, null, null, function() {
9519 modules['settingsNavigation'].loadSettingsPage(this.moduleID, 'macros');
9520 $('.RESMacroDropdown').fadeOut(100);
9522 modules['commentTools'].addButtonToMacroGroup('', addMacroButton);
9527 //Wrap the edit bar in a <div> of its own
9528 var wrappedEditBar = $("<div>").append(editBar);
9529 if (this.options.commentingAs.value && (!modules['usernameHider'].isEnabled())) {
9530 // show who we're commenting as...
9531 var commentingAs = $('<div class="commentingAs">').text('Commenting as: ' + RESUtils.loggedInUser());
9532 wrappedEditBar.append(commentingAs);
9535 this.cachedEditBar = wrappedEditBar;
9536 return this.cachedEditBar;
9538 macroDropDownTable: {},
9539 getMacroGroup: function(groupName) {
9540 //Normalize and supply a default group name{}
9541 groupName = (groupName||"").toString().trim() || "macros";
9543 if (groupName in this.macroDropDownTable) {
9544 macroGroup = this.macroDropDownTable[groupName];
9546 macroGroup = this.macroDropDownTable[groupName] = {};
9547 macroGroup.titleButton = $('<span class="RESMacroDropdownTitle">'+groupName+'</span>');
9548 macroGroup.container = $('<span class="RESMacroDropdown"><span class="RESMacroDropdownTitleOverlay">'+groupName+'</span></span>').hide();
9549 macroGroup.dropdown = $('<ul class="RESMacroDropdownList RESDropdownList"></ul>');
9550 macroGroup.container.append(macroGroup.dropdown);
9554 addButtonToMacroGroup: function(groupName, button) {
9555 var group = this.getMacroGroup(groupName);
9556 group.dropdown.append($("<li>").append(button));
9558 buildMacroDropdowns: function(editBar) {
9559 var macros = this.options.macros.value;
9561 for (var i = 0; i < macros.length; i++) {
9562 var macro = macros[i];
9564 //Confound these scoping rules
9565 (function(title, text, category, key) {
9566 var button = this.makeEditButton(title, null, key, function(button, box) {
9567 this.macroSelection(box, text, "");
9569 this.addButtonToMacroGroup(category, button);
9570 }).apply(this, macro);
9574 var macroWrapper = $('<span class="RESMacroWrappingSpan">');
9575 if ("macros" in this.macroDropDownTable) {
9576 macroWrapper.append(this.macroDropDownTable["macros"].titleButton);
9577 macroWrapper.append(this.macroDropDownTable["macros"].container);
9579 for (var category in this.macroDropDownTable) {
9580 if (category === "macros") continue;
9581 macroWrapper.append(this.macroDropDownTable[category].titleButton);
9582 macroWrapper.append(this.macroDropDownTable[category].container);
9584 editBar.append(macroWrapper);
9586 makeEditButton: function(label, title, key, handler) {
9587 if (label == null) label = "unlabeled";
9588 if (title == null) title = "";
9589 var macroButtonIndex = this.macroCallbackTable.length;
9590 var button = $("<a>").html(label).attr({
9592 href: "javascript:void(0)",
9594 "data-macro-index": macroButtonIndex
9597 if (key != null && key[0] != null) {
9598 this.macroKeyTable.push([key, macroButtonIndex]);
9600 this.macroCallbackTable[macroButtonIndex] = handler;
9603 linkSelection: function(box) {
9604 var url = prompt("Enter the URL:", "");
9606 //escape parens in url
9607 url = url.replace(/\(/, "\\(");
9608 url = url.replace( /\)/, "\\)");
9609 this.wrapSelection(box, "[", "](" + url + ")", function(text) {
9610 //escape brackets and parens in text
9611 text = text.replace(/\[/, "\\[");
9612 text = text.replace(/\]/, "\\]");
9613 text = text.replace(/\(/, "\\(");
9614 text = text.replace(/\)/, "\\)");
9619 macroSelection: function(box, macroText) {
9620 if (!this.options.keepMacroListOpen.value) $('.RESMacroDropdown').fadeOut(100);
9621 this.wrapSelection(box, macroText, "");
9623 wrapSelection: function(box, prefix, suffix, escapeFunction) {
9624 if (box == null) return;
9625 //record scroll top to restore it later.
9626 var scrollTop = box.scrollTop;
9628 //We will restore the selection later, so record the current selection.
9629 var selectionStart = box.selectionStart;
9630 var selectionEnd = box.selectionEnd;
9632 var text = box.value;
9633 var beforeSelection = text.substring(0, selectionStart);
9634 var selectedText = text.substring(selectionStart, selectionEnd);
9635 var afterSelection = text.substring(selectionEnd);
9637 //Markdown doesn't like it when you tag a word like **this **. The space messes it up. So we'll account for that because Firefox selects the word, and the followign space when you double click a word.
9638 var trailingSpace = "";
9639 var cursor = selectedText.length - 1;
9640 while (cursor > 0 && selectedText[cursor] === " ") {
9641 trailingSpace += " ";
9644 selectedText = selectedText.substring(0, cursor+1);
9646 if (escapeFunction != null) {
9647 selectedText = escapeFunction(selectedText);
9650 box.value = beforeSelection + prefix + selectedText + suffix + trailingSpace + afterSelection;
9652 box.selectionStart = selectionStart + prefix.length;
9653 box.selectionEnd = selectionEnd + prefix.length;
9655 box.scrollTop = scrollTop;
9657 wrapSelectedLines: function(box, prefix, suffix) {
9658 var scrollTop = box.scrollTop;
9659 var selectionStart = box.selectionStart;
9660 var selectionEnd = box.selectionEnd;
9662 var text = box.value;
9663 var startPosition = 0;
9664 var lines = text.split("\n");
9665 for (var i = 0; i < lines.length; i++) {
9666 var lineStart = startPosition;
9667 var lineEnd = lineStart + lines[i].length;
9668 //Check if either end of the line is within the selection
9669 if (selectionStart <= lineStart && lineStart <= selectionEnd
9670 || selectionStart <= lineEnd && lineEnd <= selectionEnd
9671 //Check if either end of the selection is within the line
9672 || lineStart <= selectionStart && selectionStart <= lineEnd
9673 || lineStart <= selectionEnd && selectionEnd <= lineEnd) {
9674 lines[i] = prefix + lines[i] + suffix;
9675 //Move the offsets separately so we don't throw off detection for the other end
9676 var startMovement = 0, endMovement = 0;
9677 if (lineStart < selectionStart) startMovement += prefix.length;
9678 if (lineEnd < selectionStart) startMovement += suffix.length;
9679 if (lineStart < selectionEnd) endMovement += prefix.length;
9680 if (lineEnd < selectionEnd) endMovement += suffix.length;
9682 selectionStart += startMovement;
9683 selectionEnd += endMovement;
9684 lineStart += prefix.length;
9685 lineEnd += prefix.length + suffix.length;
9687 //Remember the newline
9688 startPosition = lineEnd + 1;
9691 box.value = lines.join("\n");
9692 box.selectionStart = selectionStart;
9693 box.selectionEnd = selectionEnd;
9694 box.scrollTop = scrollTop;
9696 wrapSelectedWords: function(box, prefix) {
9697 var scrollTop = box.scrollTop;
9698 var selectionStart = box.selectionStart;
9699 var selectionEnd = box.selectionEnd;
9701 var text = box.value;
9702 var beforeSelection = text.substring(0, selectionStart);
9703 var selectedWords = text.substring(selectionStart, selectionEnd).split(" ");
9704 var afterSelection = text.substring(selectionEnd);
9706 var selectionModify = 0;
9708 for (i = 0; i < selectedWords.length; i++) {
9709 if (selectedWords[i] !== "") {
9710 if (selectedWords[i].indexOf("\n") !== -1)
9712 newLinePosition = selectedWords[i].lastIndexOf("\n") + 1;
9713 selectedWords[i] = selectedWords[i].substring(0, newLinePosition) + prefix + selectedWords[i].substring(newLinePosition);
9714 selectionModify += prefix.length;
9716 if (selectedWords[i].charAt(0) !== "\n") {
9717 selectedWords[i] = prefix + selectedWords[i];
9719 selectionModify += prefix.length;
9721 // If nothing is selected, stick the prefix in there and move the cursor to the right side.
9722 else if (selectedWords[i] === "" && selectedWords.length === 1) {
9723 selectedWords[i] = prefix + selectedWords[i];
9724 selectionModify += prefix.length;
9725 selectionStart += prefix.length;
9729 box.value = beforeSelection + selectedWords.join(" ") + afterSelection;
9730 box.selectionStart = selectionStart;
9731 box.selectionEnd = selectionEnd + selectionModify;
9732 box.scrollTop = scrollTop;
9735 if (typeof this.firstlod === 'undefined') {
9736 this.firstlod = true;
9737 $('body').append('<div id="RESlod" style="display: none; position: fixed; left: 0; top: 0; right: 0; bottom: 0; background-color: #ddd; opacity: 0.9; z-index: 99999;"><div style="position: relative; text-align: center; width: 400px; height: 300px; margin: auto;"><div style="font-size: 100px; margin-bottom: 10px;">ಠ\_ಠ</div> when you do this, people direct their frustrations at <b>me</b>... could we please maybe give this a rest?</div></div>');
9739 $('#RESlod').fadeIn('slow', function() {
9740 setTimeout(function() {
9741 $('#RESlod').fadeOut('slow');
9746 BACKSPACE: 8, TAB: 9, ENTER: 13,
9747 ESCAPE: 27, SPACE: 32,
9748 PAGE_UP: 33, PAGE_DOWN: 34,
9750 LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40,
9751 NUMPAD_ENTER: 108, COMMA: 188
9753 addAutoCompletePop: function() {
9755 this.autoCompleteCache = {};
9756 this.autoCompletePop = $('<div id="autocomplete_dropdown" \
9757 class="drop-choices srdrop inuse" \
9758 style="display:none;">');
9759 this.autoCompletePop.delegate(".choice", "click mousedown", function(e) {
9761 modules["commentTools"].autoCompleteHideDropdown();
9762 modules["commentTools"].autoCompleteInsert(this.innerHTML);
9764 $("body").append(this.autoCompletePop);
9766 $("body").delegate(".usertext .usertext-edit textarea, #BigText, #wiki_page_content", {
9767 keyup: this.autoCompleteTrigger,
9768 keydown: this.autoCompleteNavigate,
9769 blur: this.autoCompleteHideDropdown
9772 autoCompleteLastTarget: null,
9773 autoCompleteTrigger: function(e) {
9774 var mod = modules["commentTools"];
9775 var KEYS = mod.KEYS;
9776 //\0x08 is backspace
9777 if (/[^A-Za-z0-9 \x08]/.test(String.fromCharCode(e.keyCode))) return true;
9778 mod.autoCompleteLastTarget = this;
9779 var matchRE = /\W\/?([ru])\/([\w\.]*)$/;
9780 var matchSkipRE = /\W\/?([ru])\/([\w\.]*)\ $/;
9781 var fullText = $(this).val();
9782 var prefixText = fullText.slice(0, this.selectionStart);
9783 var match = matchRE.exec(" " + prefixText);
9784 if (match != null) {
9785 if (match[1] === "r" && mod.options.subredditAutocomplete.value == false) return;
9786 if (match[1] === "u" && mod.options.userAutocomplete.value == false) return;
9789 if(match==null || match[2] === "" || match[2].length > 10) {
9790 if (e.keyCode === KEYS.SPACE || e.keyCode === KEYS.ENTER) {
9791 var match = matchSkipRE.exec(" " + prefixText);
9793 mod.autoCompleteInsert(match[2]);
9796 return mod.autoCompleteHideDropdown();
9799 var type = match[1];
9800 var query = match[2].toLowerCase();
9801 var queryId = type+"/"+query;
9802 var cache = mod.autoCompleteCache;
9803 if (queryId in cache) {
9804 return mod.autoCompleteUpdateDropdown(cache[queryId]);
9807 RESUtils.debounce("autoComplete", 300, function() {
9809 mod.getSubredditCompletions(query);
9810 } else if (type === "u") {
9811 mod.getUserCompletions(query);
9815 getSubredditCompletions: function(query) {
9816 var mod = modules['commentTools'];
9817 if (this.options.subredditAutocomplete.value) {
9820 url: "/api/search_reddit_names.json",
9821 data: {query: query, app: "res"},
9823 success: function(data) {
9824 mod.autoCompleteCache['r/'+query] = data.names;
9825 mod.autoCompleteUpdateDropdown(data.names);
9826 mod.autoCompleteSetNavIndex(0);
9831 getUserCompletions: function(query) {
9832 if (this.options.userAutocomplete.value) {
9833 var tags = JSON.parse(RESStorage.getItem("RESmodules.userTagger.tags"));
9834 var tagNames = Object.keys(tags);
9835 var pageNames = [].map.call($(".author"), function(e) {
9838 var names = tagNames.concat(pageNames);
9839 names = names.filter(function(e, i, a) {
9840 return e.toLowerCase().indexOf(query) === 0;
9841 }).sort().reduce(function(prev, current, i, a) {
9842 //Removing duplicates
9843 if (prev[prev.length - 1] != current) {
9849 this.autoCompleteCache['u/'+query] = names;
9850 this.autoCompleteUpdateDropdown(names);
9851 this.autoCompleteSetNavIndex(0);
9854 autoCompleteNavigate: function(e) {
9855 //Don't mess with shortcuts for fancier cursor movement
9856 if (e.metaKey || e.shiftKey || e.ctrlKey || e.altKey) return;
9857 var mod = modules["commentTools"];
9858 var KEYS = mod.KEYS;
9859 var entries = mod.autoCompletePop.find("a.choice");
9860 var index = +mod.autoCompletePop.find(".selectedItem").attr("data-index");
9861 if (mod.autoCompletePop.is(':visible')) {
9862 switch (e.keyCode) {
9866 if (index < entries.length-1) index++;
9867 mod.autoCompleteSetNavIndex(index);
9872 if (index > 0) index--;
9873 mod.autoCompleteSetNavIndex(index);
9878 $(entries[index]).click();
9882 mod.autoCompleteHideDropdown();
9888 autoCompleteSetNavIndex: function(index) {
9889 var entries = modules["commentTools"].autoCompletePop.find("a.choice");
9890 entries.removeClass("selectedItem");
9891 entries.eq(index).addClass("selectedItem");
9893 autoCompleteHideDropdown: function() {
9894 modules["commentTools"].autoCompletePop.hide();
9896 autoCompleteUpdateDropdown: function(names) {
9897 var mod = modules["commentTools"];
9899 if(!names.length) return mod.autoCompleteHideDropdown();
9900 mod.autoCompletePop.empty();
9901 $.each(names.slice(0, 20), function(i, e) {
9902 mod.autoCompletePop.append('<a class="choice" data-index="'+i+'">'+e+'</a>');
9905 var textareaOffset = $(mod.autoCompleteLastTarget).offset();
9906 textareaOffset.left += $(mod.autoCompleteLastTarget).width();
9907 mod.autoCompletePop.css(textareaOffset).show();
9909 mod.autoCompleteSetNavIndex(0);
9912 autoCompleteInsert: function(inputValue) {
9913 var textarea = modules["commentTools"].autoCompleteLastTarget,
9914 caretPos = textarea.selectionStart,
9915 left = textarea.value.substr(0, caretPos),
9916 right = textarea.value.substr(caretPos);
9917 left = left.replace(/\/?([ru])\/(\w*)\ ?$/, '/$1/'+inputValue+' ');
9918 textarea.value = left + right;
9919 textarea.selectionStart = textarea.selectionEnd = left.length;
9922 findTextareaForElement: function(elem) {
9924 .closest(".usertext-edit, .RESDialogContents, .wiki-page-content")
9926 .filter("#BigText, [name=text], [name=description], [name=public_description], #wiki_page_content")
9932 modules['usernameHider'] = {
9933 moduleID: 'usernameHider',
9934 moduleName: 'Username Hider',
9935 category: 'Accounts',
9939 value: '~anonymous~',
9940 description: 'What to replace your username with, default is ~anonymous~'
9943 description: 'This module hides your real username when you\'re logged in to reddit.',
9944 isEnabled: function() {
9945 return RESConsole.getModulePrefs(this.moduleID);
9948 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i,
9949 /^https?:\/\/reddit\.com\/[-\w\.\/]*/i
9951 isMatchURL: function() {
9952 return RESUtils.isMatchURL(this.moduleID);
9954 beforeLoad: function() {
9955 if ((this.isEnabled()) && (this.isMatchURL())) {
9956 if (!RESUtils.loggedInUser(true)) {
9957 this.tryAgain = true;
9960 this.hideUsername();
9964 if ((this.isEnabled()) && (this.isMatchURL())) {
9965 if (this.tryAgain && RESUtils.loggedInUser()) {
9966 this.hideUsername();
9967 GM_addStyle(RESUtils.css);
9971 hideUsername: function() {
9972 var user = RESUtils.loggedInUser(),
9973 curatedBy = document.querySelector('.multi-details > h2 a');
9974 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;}');
9975 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;}');
9976 RESUtils.addCSS('.commentingAs:after {content: "Commenting as: '+this.options.displayText.value+'";letter-spacing:normal;font-size:12px;}');
9977 if ( modules['userHighlight'].isEnabled() ){
9978 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;}');
9979 RESUtils.addCSS('p.tagline > .submitter[href*=\'/'+user+'\']:after{ background-color:'+modules['userHighlight'].options.OPColor.value+';}');
9980 RESUtils.addCSS('p.tagline > .moderator[href*=\'/'+user+'\']:after{ background-color:'+modules['userHighlight'].options.modColor.value+';}');
9981 RESUtils.addCSS('p.tagline > .submitter[href*=\'/'+user+'\']:hover:after{ background-color:'+modules['userHighlight'].options.OPColorHover.value+';}');
9982 RESUtils.addCSS('p.tagline > .moderator[href*=\'/'+user+'\']:hover:after{ background-color:'+modules['userHighlight'].options.modColorHover.value+';}');
9984 if ( curatedBy && curatedBy.href.slice(-(user.length+1)) === '/' + user ){
9985 curatedBy.textContent = 'curated by /u/' + this.options.displayText.value;
9991 /* siteModule format:
9993 //Initialization method for things that cannot be performed inline. The method
9994 //is required to be present, but it can be empty
9997 //Returns true/false to indicate whether the siteModule will attempt to handle the link
9998 //the only parameter is the anchor element
9999 //returns true or false
10000 detect: function(element) {return true/false;},
10002 //This is where links are parsed, cache checks are made, and XHR is performed.
10003 //the only parameter is the anchor element
10004 //The method is in a jQuery Deferred chain and will be followed by handleInfo.
10005 //A new $.Deferred object should be created and resolved/rejected as necessary and then reterned.
10006 //If resolving, the element should be passed along with whatever data is required.
10007 handleLink: function(element) {},
10009 //This is were the embedding information is added to the link
10010 //handleInfo sits in the Deferred chain after handLink
10011 //and should recieve both the element and a data object from handleLink
10012 //the first parameter should the same anchor element passed to handleLink
10013 //the second parameter should module specific data
10014 //A new $.Deferred object should be created and resolved/rejected as necessary and then reterned
10015 //If resolving, the element should be passed
10016 handleInfo: function(elem, info) {}
10019 Embedding infomation:
10020 all embedding information (except 'site') is to be attatched the
10021 html anchor in the handleInfo function
10024 'IMAGE' for single images | 'GALLERY' for image galleries | 'TEXT' html/text to be displayed
10026 if type is TEXT then src is HTML (be carefull what is accepted here)
10027 if type is IMAGE then src is an image URL string
10028 if type is GALLERY then src is an array of objects with the following properties:
10029 required src: URL of the image
10030 optional href: URL of the page containing the image (per image)
10031 optional title: string to displayed directly above the image (per image)
10032 optional caption: string to be displayed directly below the image (per image)
10033 optional imageTitle:
10034 string to be displayed above the image (gallery level).
10036 string to be displayed below the image
10038 string to be displayed below caption
10039 optional galleryStart:
10040 zero-indexed page number to open the gallery to
10042 modules['showImages'] = {
10043 moduleID: 'showImages',
10044 moduleName: 'Inline Image Viewer',
10050 description: 'Max width of image displayed onscreen'
10055 description: 'Max height of image displayed onscreen'
10060 description: 'Open images in a new tab/window when clicked?'
10065 description: 'If checked, do not show images marked NSFW.'
10067 autoExpandSelfText: {
10070 description: 'When loading selftext from an Aa+ expando, auto reveal images.'
10075 description: 'Allow dragging to resize/zoom images.'
10080 description: 'Mark links visited when you view images (does eat some resources).'
10086 {name: 'Add links to history', value: 'add'},
10087 {name: 'Color links, but do not add to history', value: 'color'},
10088 {name: 'Do not add or color links.', value: 'none'}
10090 description: 'Keeps NSFW links from being added to your browser history <span style="font-style: italic">by the markVisited feature</span>.<br/>\
10091 <span style="font-style: italic">If you chose the second option, then links will be blue again on refresh.</span><br/>\
10092 <span style="color: red">This does not change your basic browser behavior.\
10093 If you click on a link then it will still be added to your history normally.\
10094 This is not a substitute for using your browser\'s privacy mode.</span>'
10096 ignoreDuplicates: {
10099 description: 'Do not create expandos for images that appear multiple times in a page.'
10101 displayImageCaptions: {
10104 description: 'Retrieve image captions/attribution information.'
10109 description: 'Display all images at once in a \'filmstrip\' layout, rather than the default navigable \'slideshow\' style.'
10112 description: 'Opens images inline in your browser with the click of a button. Also has configuration options, check it out!',
10113 isEnabled: function() {
10114 return RESConsole.getModulePrefs(this.moduleID);
10117 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\_\?=]*/i
10120 /^https?:\/\/([a-z]+)\.reddit\.com\/ads\/[-\w\.\_\?=]*/i,
10121 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*\/submit\/?$/i
10123 isMatchURL: function() {
10124 return RESUtils.isMatchURL(this.moduleID);
10126 beforeLoad: function() {
10127 if ((this.isEnabled()) && (this.isMatchURL())) {
10128 if (!this.options.displayImageCaptions.value) {
10129 RESUtils.addCSS('.imgTitle, .imgCaptions { display: none; }');
10134 if ((this.isEnabled()) && (this.isMatchURL())) {
10136 this.imageList = [];
10137 this.imagesRevealed = {};
10138 this.dupeAnchors = 0;
10140 true: show all images
10141 false: hide all images
10142 'any string': display images match the tab
10144 this.currentImageTab = false;
10145 this.customImageTabs = {};
10147 if (this.options.markVisited.value) {
10148 // we only need this iFrame hack if we're unable to add to history directly, which Firefox addons and Chrome can do.
10149 if (!BrowserDetect.isChrome() && !BrowserDetect.isFirefox()) {
10150 this.imageTrackFrame = document.createElement('iframe');
10151 this.imageTrackFrame.addEventListener('load', function() {
10152 setTimeout(modules['showImages'].imageTrackShift, 300);
10154 this.imageTrackFrame.style.display = 'none';
10155 this.imageTrackFrame.style.width = '0px';
10156 this.imageTrackFrame.style.height = '0px';
10157 document.body.appendChild(this.imageTrackFrame);
10159 this.imageTrackStack = [];
10162 //set up all site modules
10163 for (var key in this.siteModules) {
10164 this.siteModules[key].go();
10166 this.scanningForImages = false;
10168 RESUtils.watchForElement('siteTable', modules['showImages'].findAllImages);
10169 RESUtils.watchForElement('selfText', modules['showImages'].findAllImagesInSelfText);
10170 RESUtils.watchForElement('newComments', modules['showImages'].findAllImagesInSelfText);
10172 this.createImageButtons();
10173 this.findAllImages();
10174 document.addEventListener('dragstart', function(){return false;}, false);
10177 findAllImagesInSelfText: function(ele) {
10178 modules['showImages'].findAllImages(ele, true);
10180 createImageButtons: function() {
10181 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'))) {
10182 var hbl = document.body.querySelector('#header-bottom-left');
10184 var mainMenuUL = document.createElement('ul');
10185 mainMenuUL.setAttribute('class','tabmenu viewimages');
10186 mainMenuUL.setAttribute('style','display: inline-block');
10187 hbl.appendChild(mainMenuUL);
10190 var mainMenuUL = document.body.querySelector('#header-bottom-left ul.tabmenu');
10193 var li = document.createElement('li');
10194 var a = document.createElement('a');
10195 var text = document.createTextNode('scanning for images...');
10196 this.scanningForImages = true;
10198 a.href = 'javascript:void(0);';
10199 a.id = 'viewImagesButton';
10200 a.addEventListener('click', function(e) {
10201 e.preventDefault();
10202 if (!modules['showImages'].scanningForImages) {
10203 modules['showImages'].setShowImages(null, 'image');
10206 a.appendChild(text);
10208 mainMenuUL.appendChild(li);
10209 this.viewImageButton = a;
10211 To enable custom image tabs for a subreddit start by adding `[](#/RES_SR_Config/ImageTabs?)` to the markdown code of the sidebar.
10212 This should not have any visible effect on the HTML.
10213 Right now no options have been configured, so there won't be any new tabs.
10214 You can add up to 8 tabs in the following manner:
10215 A tab is defined by a label and a tag list separated by an equals sign like this: `LABEL=TAGLIST`
10216 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.
10217 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)
10219 The the tab definitions are joined by ampersands (`&`).
10220 Labels appear to the right of the "view images" button and are surrounded by `[]` brackets.
10221 Post titles are searched for any place that an entry in the tag list appears surrounded by any kind of bracket <>, [], (), {}.
10222 Tags are not case sensitive and whitespace is permitted between the brackets and the tag.
10224 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'`.
10226 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.
10229 A hypothetical setup for /r/minecraft that creates tabs for builds, mods, and texture packs:
10231 [](#/RES_SR_Config/ImageTabs?build=build,project&mod=mod&texture%20pack=texture,textures,pack,texture%20pack)
10233 To duplicate the behavior originally used for /r/gonewild you would use:
10235 [](#/RES_SR_Config/ImageTabs?m=m,man,male&f=f,fem,female)
10238 var tabConfig = document.querySelector('.side .md a[href^="#/RES_SR_Config/ImageTabs"]');
10239 //This is hardcoded until the mods of /r/gonewild add the tag
10240 if (!tabConfig && RESUtils.currentSubreddit('gonewild')) {
10241 tabConfig = $('<a href="#/RES_SR_Config/ImageTabs?m=m,man,male&f=f,fem,female">')[0];
10247 var switchCount = 0;
10249 var whitelist = /^[A-Za-z0-9_ \-]{1,32}$/;
10250 var configString = tabConfig.hash.match(/\?(.*)/);
10251 if (configString != null) {
10252 var pairs = configString[1].split('&');
10253 for (var i = 0; i < pairs.length && switchCount < 8; i++) {
10254 var pair = pairs[i].split('=');
10255 if (pair.length !== 2) continue;
10256 var label = decodeURIComponent(pair[0]);
10257 if (!whitelist.test(label)) continue;
10258 var parts = pair[1].split(',');
10259 var acceptedParts = [];
10260 for (var j = 0; j < parts.length && acceptedParts.length < 8; j++) {
10261 var part = decodeURIComponent(parts[j]);
10262 if (!whitelist.test(part)) continue;
10263 else acceptedParts.push(part);
10265 if (acceptedParts.length > 0) {
10266 if (!(label in switches)) switchCount++;
10267 switches[label] = acceptedParts;
10271 if (switchCount > 0) {
10272 for (var key in switches) {
10273 this.customImageTabs[key] = new RegExp('[\\[\\{\\<\\(]\s*('+switches[key].join('|')+')\s*[\\]\\}\\>\\)]','i');
10278 if (!/comments\/[-\w\.\/]/i.test(location.href)) {
10279 for (var mode in this.customImageTabs) {
10280 var li = document.createElement('li');
10281 var a = document.createElement('a');
10282 var text = document.createTextNode('['+mode+']');
10283 a.href = 'javascript:void(0);';
10284 a.className = 'RESTab-'+mode.toLowerCase().replace(/- /g, '_');
10285 a.addEventListener('click', (function(mode) {
10286 return function(e) {
10287 e.preventDefault();
10288 modules['showImages'].setShowImages(mode);
10292 a.appendChild(text);
10294 mainMenuUL.appendChild(li);
10299 setShowImages: function(newImageTab, type) {
10300 type = type || 'image';
10301 if (newImageTab == null) {
10302 //This is for the all images button
10303 //If we stored `true` then toggle to false, in all other cases turn it to true
10304 if (this.currentImageTab == true) {
10305 this.currentImageTab = false;
10307 this.currentImageTab = true;
10309 } else if (this.currentImageTab == newImageTab) {
10310 //If they are the same, turn it off
10311 this.currentImageTab = false;
10312 } else if (newImageTab in this.customImageTabs) {
10313 //If the tab is defined, switch to it
10314 this.currentImageTab = newImageTab;
10316 //Otherwise ignore it
10319 this.updateImageButtons();
10320 this.updateRevealedImages(type);
10322 updateImageButtons: function() {
10323 var imgCount = this.imageList.length;
10324 var showHideText = 'view';
10325 if (this.currentImageTab == true) {
10326 showHideText = 'hide';
10328 if (typeof this.viewImageButton !== 'undefined') {
10329 var buttonText = showHideText + ' images ';
10330 if (! RESUtils.currentSubreddit('dashboard')) buttonText += '(' + imgCount + ')';
10331 $(this.viewImageButton).text(buttonText);
10334 updateRevealedImages: function(type) {
10335 for (var i = 0, len = this.imageList.length; i < len; i++) {
10336 var image = this.imageList[i];
10337 if ($(image).hasClass(type)) {
10338 this.revealImage(image, this.findImageFilter(image.imageLink));
10342 findImageFilter: function(image) {
10343 var isMatched = false;
10344 if (typeof this.currentImageTab === 'boolean') {
10345 //booleans indicate show all or nothing
10346 isMatched = this.currentImageTab;
10347 } else if (this.currentImageTab in this.customImageTabs) {
10348 var re = this.customImageTabs[this.currentImageTab];
10349 isMatched = re.test(image.text);
10351 //If false then there is no need to go through the NSFW filter
10352 if (!isMatched) return false;
10354 image.NSFW = false;
10355 if (this.options.hideNSFW.value) {
10356 image.NSFW = /nsfw/i.test(image.text);
10359 return !image.NSFW;
10361 findAllImages: function(elem, isSelfText) {
10362 modules['showImages'].scanningForImages = true;
10363 if (elem == null) {
10364 elem = document.body;
10366 // get elements common across all pages first...
10367 // if we're on a comments page, get those elements too...
10368 var commentsre = /comments\/[-\w\.\/]/i;
10369 var userre = /user\/[-\w\.\/]/i;
10370 modules['showImages'].scanningSelfText = false;
10371 var allElements = [];
10372 if (commentsre.test(location.href) || userre.test(location.href)) {
10373 allElements = elem.querySelectorAll('#siteTable a.title, .expando .usertext-body > div.md a, .content .usertext-body > div.md a');
10374 } else if (isSelfText) {
10375 // We're scanning newly opened (from an expando) selftext...
10376 allElements = elem.querySelectorAll('.usertext-body > div.md a');
10377 modules['showImages'].scanningSelfText = true;
10379 allElements = elem.querySelectorAll('#siteTable A.title');
10382 if (RESUtils.pageType() === 'comments') {
10383 RESUtils.forEachChunked(allElements, 15, 1000, function(element, i, array) {
10384 modules['showImages'].checkElementForImage(element);
10385 if (i >= array.length - 1) {
10386 modules['showImages'].scanningSelfText = false;
10387 modules['showImages'].scanningForImages = false;
10388 modules['showImages'].updateImageButtons(modules['showImages'].imageList.length);
10392 var chunkLength = allElements.length;
10393 for (var i = 0; i < chunkLength; i++) {
10394 modules['showImages'].checkElementForImage(allElements[i]);
10396 modules['showImages'].scanningSelfText = false;
10397 modules['showImages'].scanningForImages = false;
10398 modules['showImages'].updateImageButtons(modules['showImages'].imageList.length);
10401 checkElementForImage: function(elem) {
10402 if (this.options.hideNSFW.value) {
10403 if (elem.classList.contains('title')) {
10404 elem.NSFW = elem.parentNode.parentNode.parentNode.classList.contains('over18');
10409 var href = elem.href;
10410 if ((!elem.classList.contains('imgScanned') && (typeof this.imagesRevealed[href] === 'undefined' || !this.options.ignoreDuplicates.value || (RESUtils.currentSubreddit('dashboard'))) && href !== null) || this.scanningSelfText) {
10411 elem.classList.add('imgScanned');
10412 this.dupeAnchors++;
10413 var siteFound = false;
10414 if (siteFound = this.siteModules['default'].detect(elem)) {
10415 elem.site = 'default';
10418 for (var site in this.siteModules) {
10419 if (site === 'default') continue;
10420 if (this.siteModules[site].detect(elem)) {
10427 if (siteFound && !elem.NSFW) {
10428 this.imagesRevealed[href] = this.dupeAnchors;
10429 var siteMod = this.siteModules[elem.site];
10430 $.Deferred().resolve(elem).then(siteMod.handleLink).then(siteMod.handleInfo).
10431 then(this.createImageExpando, function(){
10432 console.error.apply(console, arguments);
10435 } else if (!elem.classList.contains('imgScanned')) {
10436 var textFrag = document.createElement('span');
10437 textFrag.setAttribute('class','RESdupeimg');
10438 $(textFrag).html(' <a class="noKeyNav" href="#img'+escapeHTML(this.imagesRevealed[href])+'" title="click to scroll to original">[RES ignored duplicate image]</a>');
10439 insertAfter(elem, textFrag);
10442 createImageExpando: function(elem) {
10443 var mod = modules['showImages'];
10444 if (!elem) return false;
10445 var href = elem.href;
10446 if (!href) return false;
10447 //This should not be reached in the case of duplicates
10448 elem.name = 'img'+mod.imagesRevealed[href];
10450 //expandLink aka the expando button
10451 var expandLink = document.createElement('a');
10452 expandLink.className = 'toggleImage expando-button collapsedExpando';
10453 if (elem.type === 'IMAGE') expandLink.className += ' image';
10454 if (elem.type === 'GALLERY') expandLink.className += ' image gallery';
10455 if (elem.type === 'TEXT') expandLink.className += ' selftext collapsed';
10456 if (elem.type === 'VIDEO') expandLink.className += ' video collapsed';
10457 if (elem.type === 'AUDIO') expandLink.className += ' video collapsed'; // yes, still class "video", that's what reddit uses.
10458 if (elem.type === 'NOEMBED') expandLink.className += ' '+elem.expandoClass;
10460 if (elem.type === 'GALLERY' && elem.src && elem.src.length) expandLink.setAttribute('title', elem.src.length + ' items in gallery');
10461 $(expandLink).html(' ');
10462 expandLink.addEventListener('click', function(e) {
10463 e.preventDefault();
10464 modules['showImages'].revealImage(e.target, (e.target.classList.contains('collapsedExpando')));
10466 var preNode = null;
10467 if (elem.parentNode.classList.contains('title')) {
10468 preNode = elem.parentNode;
10469 expandLink.classList.add('linkImg');
10472 expandLink.classList.add('commentImg');
10474 insertAfter(preNode, expandLink);
10476 * save the link element for later use since some extensions
10477 * like web of trust can place other elements in places that
10478 * confuse the old method
10480 expandLink.imageLink = elem;
10481 mod.imageList.push(expandLink);
10483 if (mod.scanningSelfText && mod.options.autoExpandSelfText.value) {
10484 mod.revealImage(expandLink, true);
10486 // this may have come from an asynchronous call, in which case it'd get missed by findAllImages, so
10487 // if all images are supposed to be visible, expand this link now.
10488 mod.revealImage(expandLink, mod.findImageFilter(expandLink.imageLink));
10492 if (mod.scanningForImages == false) {
10493 // also since this may have come from an asynchronous call, we need to update the view images count.
10494 mod.updateImageButtons(mod.imageList.length);
10497 revealImage: function(expandoButton, showHide) {
10498 if ((!expandoButton) || (! $(expandoButton).is(':visible'))) return false;
10499 // showhide = false means hide, true means show!
10501 var imageLink = expandoButton.imageLink;
10502 if (typeof this.siteModules[imageLink.site] === 'undefined') {
10503 console.log('something went wrong scanning image from site: ' + imageLink.site);
10506 if (expandoButton.expandoBox && expandoButton.expandoBox.classList.contains('madeVisible')) {
10508 $(expandoButton).removeClass('expanded').addClass('collapsed collapsedExpando');
10509 expandoButton.expandoBox.style.display = 'none';
10510 if (imageLink.type === 'AUDIO' || imageLink.type === 'VIDEO') {
10511 var mediaTag = expandoButton.expandoBox.querySelector(imageLink.type);
10515 $(expandoButton).addClass('expanded').removeClass('collapsed collapsedExpando');
10516 expandoButton.expandoBox.style.display = 'block';
10517 var associatedImage = $(expandoButton).data('associatedImage');
10518 if (associatedImage) {
10519 modules['showImages'].syncPlaceholder(associatedImage);
10522 this.handleSRStyleToggleVisibility();
10523 } else if (showHide) {
10524 //TODO: flash, custom
10525 switch (imageLink.type) {
10528 this.generateImageExpando(expandoButton);
10531 this.generateTextExpando(expandoButton);
10534 this.generateVideoExpando(expandoButton, imageLink.mediaOptions);
10537 this.generateAudioExpando(expandoButton);
10540 this.generateNoEmbedExpando(expandoButton);
10545 generateImageExpando: function(expandoButton) {
10546 var imageLink = expandoButton.imageLink;
10547 var which = imageLink.galleryStart || 0;
10549 var imgDiv = document.createElement('div');
10550 imgDiv.classList.add('madeVisible');
10551 imgDiv.currentImage = which;
10552 imgDiv.sources = [];
10554 // Test for a single image or an album/array of image
10555 if (Array.isArray(imageLink.src)) {
10556 imgDiv.sources = imageLink.src;
10558 // Also preload images for an album
10559 this.preloadImages(imageLink.src, 0);
10561 // Only the image is left to display, pack it like a single-image album with no caption or title
10562 singleImage = {src:imageLink.src,href:imageLink.href};
10563 imgDiv.sources[0] = singleImage;
10566 if ('imageTitle' in imageLink) {
10567 var header = document.createElement('h3');
10568 header.classList.add('imgTitle');
10569 $(header).safeHtml(imageLink.imageTitle);
10570 imgDiv.appendChild(header);
10573 if ('imgCaptions' in imageLink) {
10574 var captions = document.createElement('div');
10575 captions.className = 'imgCaptions';
10576 $(captions).safeHtml(imageLink.caption);
10577 imgDiv.appendChild(captions);
10580 if ('credits' in imageLink) {
10581 var credits = document.createElement('div');
10582 credits.className = 'imgCredits';
10583 $(credits).safeHtml(imageLink.credits);
10584 imgDiv.appendChild(credits);
10587 switch(imageLink.type){
10589 if (this.options.loadAllInAlbum.value) {
10590 if (imgDiv.sources.length > 1) {
10591 var albumLength = " (" + imgDiv.sources.length + " images)";
10592 $(header).append(albumLength);
10595 for (var imgNum = 0; imgNum < imgDiv.sources.length; imgNum++) {
10596 addImage(imgDiv, imgNum, this);
10600 // If we're using the traditional album view, add the controls then fall through to add the IMAGE
10601 var controlWrapper = document.createElement('div');
10602 controlWrapper.className = 'RESGalleryControls';
10604 var leftButton = document.createElement("a");
10605 leftButton.className = 'previous noKeyNav';
10606 leftButton.addEventListener('click', function(e){
10607 var topWrapper = e.target.parentElement.parentElement;
10608 if (topWrapper.currentImage === 0) {
10609 topWrapper.currentImage = topWrapper.sources.length-1;
10611 topWrapper.currentImage -= 1;
10613 adjustGalleryDisplay(topWrapper);
10615 controlWrapper.appendChild(leftButton);
10617 var posLabel = document.createElement('span');
10618 posLabel.className = 'RESGalleryLabel';
10619 var niceWhich = ((which+1 < 10)&&(imgDiv.sources.length >= 10)) ? '0'+(which+1) : (which+1);
10620 if (imgDiv.sources.length) {
10621 posLabel.textContent = niceWhich + " of " + imgDiv.sources.length;
10623 posLabel.textContent = "Whoops, this gallery seems to be empty.";
10625 controlWrapper.appendChild(posLabel);
10627 var rightButton = document.createElement("a");
10628 rightButton.className = 'next noKeyNav';
10629 rightButton.addEventListener('click', function(e){
10630 var topWrapper = e.target.parentElement.parentElement;
10631 if (topWrapper.currentImage == topWrapper.sources.length-1) {
10632 topWrapper.currentImage = 0;
10634 topWrapper.currentImage += 1;
10636 adjustGalleryDisplay(topWrapper);
10638 controlWrapper.appendChild(rightButton);
10640 if (!imgDiv.sources.length) {
10641 $(leftButton).css('visibility','hidden');
10642 $(rightButton).css('visibility','hidden');
10645 imgDiv.appendChild(controlWrapper);
10649 addImage(imgDiv, which, this);
10652 function addImage(container, sourceNumber, thisHandle) {
10653 var sourceImage = container.sources[sourceNumber];
10655 var paragraph = document.createElement('p');
10657 if (!sourceImage) {
10660 if ('title' in sourceImage) {
10661 var imageTitle = document.createElement('h4');
10662 imageTitle.className = 'imgCaptions';
10663 $(imageTitle).safeHtml(sourceImage.title);
10664 paragraph.appendChild(imageTitle);
10667 if ('caption' in sourceImage) {
10668 var imageCaptions = document.createElement('div');
10669 imageCaptions.className = 'imgCaptions';
10670 $(imageCaptions).safeHtml(sourceImage.caption);
10671 paragraph.appendChild(imageCaptions);
10674 var imageAnchor = document.createElement('a');
10675 imageAnchor.classList.add('madeVisible');
10676 imageAnchor.href = sourceImage.href;
10677 if (thisHandle.options.openInNewWindow.value) {
10678 imageAnchor.target ='_blank';
10681 var image = document.createElement('img');
10682 $(expandoButton).data('associatedImage', image);
10683 //Unfortunately it is impossible to use a global event handler for these.
10684 image.onerror = function() {
10685 image.classList.add("RESImageError");
10687 image.onload = function() {
10688 image.classList.remove("RESImageError");
10690 image.classList.add('RESImage');
10691 image.id = 'RESImage-' + RESUtils.randomHash();
10692 image.src = sourceImage.src;
10693 image.title = 'drag to resize';
10694 image.style.maxWidth = thisHandle.options.maxWidth.value + 'px';
10695 image.style.maxHeight = thisHandle.options.maxHeight.value + 'px';
10696 imageAnchor.appendChild(image);
10697 modules['showImages'].setPlaceholder(image);
10698 thisHandle.makeImageZoomable(image);
10699 thisHandle.trackImageLoad(imageLink, image);
10700 paragraph.appendChild(imageAnchor);
10702 container.appendChild(paragraph);
10705 //Adjusts the images for the gallery navigation buttons as well as the "n of m" display.
10706 function adjustGalleryDisplay(topLevel) {
10707 var source = topLevel.sources[topLevel.currentImage];
10708 var image = topLevel.querySelector('img.RESImage');
10709 var imageAnchor = image.parentElement;
10710 var paragraph = imageAnchor.parentElement;
10711 image.src = source.src;
10712 imageAnchor.href = source.href || imageLink.href;
10713 var paddedImageNumber = ((topLevel.currentImage+1 < 10)&&(imgDiv.sources.length >= 10)) ? '0'+(topLevel.currentImage+1) : topLevel.currentImage+1;
10714 if (imgDiv.sources.length) {
10715 topLevel.querySelector('.RESGalleryLabel').textContent = (paddedImageNumber+" of "+imgDiv.sources.length);
10717 topLevel.querySelector('.RESGalleryLabel').textContent = "Whoops, this gallery seems to be empty.";
10719 if (topLevel.currentImage === 0) {
10720 leftButton.classList.add('end');
10721 rightButton.classList.remove('end');
10722 } else if (topLevel.currentImage === topLevel.sources.length-1) {
10723 leftButton.classList.remove('end');
10724 rightButton.classList.add('end');
10726 leftButton.classList.remove('end');
10727 rightButton.classList.remove('end');
10730 $(paragraph).find('.imgCaptions').empty();
10731 var imageTitle = paragraph.querySelector('h4.imgCaptions');
10732 if (imageTitle) $(imageTitle).safeHtml(source.title);
10733 var imageCaptions = paragraph.querySelector('div.imgCaptions');
10734 if (imageCaptions) $(imageCaptions).safeHtml(source.caption);
10737 if (expandoButton.classList.contains('commentImg')) {
10738 insertAfter(expandoButton, imgDiv);
10740 expandoButton.parentNode.appendChild(imgDiv);
10742 expandoButton.expandoBox = imgDiv;
10744 expandoButton.classList.remove('collapsedExpando');
10745 expandoButton.classList.add('expanded');
10748 * Recursively loads the images synchronously.
10750 preloadImages: function(srcs, i) {
10754 img.onload = img.onerror = function(){
10756 if(typeof srcs[_i] === 'undefined'){
10759 _this.preloadImages(srcs, _i);
10761 delete img; // Delete the image element from the DOM to stop the RAM usage getting to high.
10763 img.src = srcs[i].src;
10765 generateTextExpando: function(expandoButton) {
10766 var imageLink = expandoButton.imageLink;
10767 var wrapperDiv = document.createElement('div');
10768 wrapperDiv.className = 'usertext';
10770 var imgDiv = document.createElement('div');
10771 imgDiv.className = 'madeVisible usertext-body';
10773 var header = document.createElement('h3');
10774 header.className = 'imgTitle';
10775 $(header).safeHtml(imageLink.imageTitle);
10776 imgDiv.appendChild(header);
10778 var text = document.createElement('div');
10779 text.className = 'md';
10780 $(text).safeHtml(imageLink.src);
10781 imgDiv.appendChild(text);
10783 var captions = document.createElement('div');
10784 captions.className = 'imgCaptions';
10785 $(captions).safeHtml(imageLink.caption);
10786 imgDiv.appendChild(captions);
10788 if ('credits' in imageLink) {
10789 var credits = document.createElement('div');
10790 credits.className = 'imgCredits';
10791 $(credits).safeHtml(imageLink.credits);
10792 imgDiv.appendChild(credits);
10795 wrapperDiv.appendChild(imgDiv);
10796 if (expandoButton.classList.contains('commentImg')) {
10797 insertAfter(expandoButton, wrapperDiv);
10799 expandoButton.parentNode.appendChild(wrapperDiv);
10801 expandoButton.expandoBox = imgDiv;
10803 expandoButton.classList.remove('collapsedExpando');
10804 expandoButton.classList.remove('collapsed');
10805 expandoButton.classList.add('expanded');
10807 //TODO: Decide how to handle history for this.
10808 //Selfposts already don't mark it, so either don't bother or add marking for selfposts.
10810 generateVideoExpando: function(expandoButton, options) {
10811 var imageLink = expandoButton.imageLink;
10812 var wrapperDiv = document.createElement('div');
10813 wrapperDiv.className = 'usertext';
10815 var imgDiv = document.createElement('div');
10816 imgDiv.className = 'madeVisible usertext-body';
10818 var header = document.createElement('h3');
10819 header.className = 'imgTitle';
10820 $(header).safeHtml(imageLink.imageTitle);
10821 imgDiv.appendChild(header);
10823 var video = document.createElement('video');
10824 video.addEventListener('click', modules['showImages'].handleVideoClick);
10825 video.setAttribute('controls','');
10826 video.setAttribute('preload','');
10828 if (options.autoplay) {
10829 video.setAttribute('autoplay','');
10831 if (options.muted) {
10832 video.setAttribute('muted','');
10834 if (options.loop) {
10835 video.setAttribute('loop','');
10838 var sourcesHTML = "",
10839 sources = $(imageLink).data('sources'),
10842 for (var i=0, len=sources.length; i<len; i++) {
10843 source = sources[i];
10844 sourceEle = document.createElement('source');
10845 sourceEle.src = source.file;
10846 sourceEle.type = source.type;
10847 $(video).append(sourceEle);
10850 imgDiv.appendChild(video);
10852 if ('credits' in imageLink) {
10853 var credits = document.createElement('div');
10854 credits.className = 'imgCredits';
10855 $(credits).safeHtml(imageLink.credits);
10856 imgDiv.appendChild(credits);
10859 wrapperDiv.appendChild(imgDiv);
10860 if (expandoButton.classList.contains('commentImg')) {
10861 insertAfter(expandoButton, wrapperDiv);
10863 expandoButton.parentNode.appendChild(wrapperDiv);
10865 expandoButton.expandoBox = imgDiv;
10867 expandoButton.classList.remove('collapsedExpando');
10868 expandoButton.classList.remove('collapsed');
10869 expandoButton.classList.add('expanded');
10871 modules['showImages'].trackImageLoad(imageLink, video);
10874 generateAudioExpando: function(expandoButton) {
10875 var imageLink = expandoButton.imageLink;
10876 var wrapperDiv = document.createElement('div');
10877 wrapperDiv.className = 'usertext';
10879 var imgDiv = document.createElement('div');
10880 imgDiv.className = 'madeVisible usertext-body';
10882 var header = document.createElement('h3');
10883 header.className = 'imgTitle';
10884 $(header).safeHtml(imageLink.imageTitle);
10885 imgDiv.appendChild(header);
10887 var audio = document.createElement('audio');
10888 audio.addEventListener('click', modules['showImages'].handleaudioClick);
10889 audio.setAttribute('controls','');
10890 // TODO: add mute/unmute control, play/pause control.
10892 var sourcesHTML = "",
10893 sources = $(imageLink).data('sources'),
10896 for (var i=0, len=sources.length; i<len; i++) {
10897 source = sources[i];
10898 sourceEle = document.createElement('source');
10899 sourceEle.src = source.file;
10900 sourceEle.type = source.type;
10901 $(audio).append(sourceEle);
10904 imgDiv.appendChild(audio);
10906 if ('credits' in imageLink) {
10907 var credits = document.createElement('div');
10908 credits.className = 'imgCredits';
10909 $(credits).safeHtml(imageLink.credits);
10910 imgDiv.appendChild(credits);
10913 wrapperDiv.appendChild(imgDiv);
10914 if (expandoButton.classList.contains('commentImg')) {
10915 insertAfter(expandoButton, wrapperDiv);
10917 expandoButton.parentNode.appendChild(wrapperDiv);
10919 expandoButton.expandoBox = imgDiv;
10921 expandoButton.classList.remove('collapsedExpando');
10922 expandoButton.classList.remove('collapsed');
10923 expandoButton.classList.add('expanded');
10925 modules['showImages'].trackImageLoad(imageLink, audio);
10927 handleVideoClick: function(e) {
10928 // for now, this does nothing, because apparently HTML5 video
10929 // doesn't have a way to detect clicks of native controls via
10930 // javascript, which means that even if you're muting/unmuting,
10931 // changing volume etc this event fires and will play/pause the
10932 // video, but e.target and e.currentTarget still point to the video
10933 // and not the controls... yuck.
10935 // if (e.target.paused) {
10936 // e.target.play();
10939 // e.target.pause();
10942 generateNoEmbedExpando: function(expandoButton) {
10943 var imageLink = expandoButton.imageLink,
10944 siteMod = imageLink.siteMod,
10945 apiURL = 'http://noembed.com/embed?url=' + imageLink.src,
10946 def = $.Deferred();
10948 GM_xmlhttpRequest({
10951 // aggressiveCache: true,
10952 onload: function(response) {
10954 var json = JSON.parse(response.responseText);
10955 siteMod.calls[apiURL] = json;
10956 modules['showImages'].handleNoEmbedQuery(expandoButton, json);
10957 def.resolve(elem, json);
10959 siteMod.calls[apiURL] = null;
10963 onerror: function(response) {
10968 handleNoEmbedQuery: function(expandoButton, response) {
10969 var imageLink = expandoButton.imageLink;
10971 var wrapperDiv = document.createElement('div');
10972 wrapperDiv.className = 'usertext';
10974 var noEmbedFrame = document.createElement('iframe');
10975 // not all noEmbed responses have a height and width, so if
10976 // this siteMod has a width and/or height set, use them.
10977 if (imageLink.siteMod.width) {
10978 noEmbedFrame.setAttribute('width', imageLink.siteMod.width);
10980 if (imageLink.siteMod.height) {
10981 noEmbedFrame.setAttribute('height', imageLink.siteMod.height);
10983 if (imageLink.siteMod.urlMod) {
10984 noEmbedFrame.setAttribute('src', imageLink.siteMod.urlMod(response.url));
10986 for (key in response) {
10989 if (!noEmbedFrame.hasAttribute('src')) {
10990 noEmbedFrame.setAttribute('src', response[key]);
10994 noEmbedFrame.setAttribute('width', response[key]);
10997 noEmbedFrame.setAttribute('height', response[key]);
11001 noEmbedFrame.className = 'madeVisible usertext-body';
11003 wrapperDiv.appendChild(noEmbedFrame);
11004 if (expandoButton.classList.contains('commentImg')) {
11005 insertAfter(expandoButton, wrapperDiv);
11007 expandoButton.parentNode.appendChild(wrapperDiv);
11010 expandoButton.expandoBox = noEmbedFrame;
11012 expandoButton.classList.remove('collapsedExpando');
11013 expandoButton.classList.remove('collapsed');
11014 expandoButton.classList.add('expanded');
11016 modules['showImages'].trackImageLoad(imageLink, video);
11018 trackImageLoad: function(link, image) {
11019 if (modules['showImages'].options.markVisited.value) {
11020 var isNSFW = $(link).closest('.thing').is('.over18');
11021 var sfwMode = modules['showImages'].options['sfwHistory'].value;
11023 if ((BrowserDetect.isChrome()) || (BrowserDetect.isFirefox())) {
11024 var url = link.historyURL || link.href;
11025 if (!isNSFW || sfwMode !== 'none') link.classList.add('visited');
11026 if (!isNSFW || sfwMode === 'add') {
11027 modules['showImages'].imageTrackStack.push(url);
11028 if (modules['showImages'].imageTrackStack.length === 1) setTimeout(modules['showImages'].imageTrackShift, 300);
11031 image.addEventListener('load', function(e) {
11032 var url = link.historyURL || link.href;
11033 if (!isNSFW || sfwMode !== 'none') link.classList.add('visited');
11034 if (!isNSFW || sfwMode === 'add') {
11035 modules['showImages'].imageTrackStack.push(url);
11036 if (modules['showImages'].imageTrackStack.length === 1) setTimeout(modules['showImages'].imageTrackShift, 300);
11042 image.addEventListener('load', function(e) {
11043 modules['showImages'].handleSRStyleToggleVisibility(e.target);
11046 imageTrackShift: function() {
11047 var url = modules['showImages'].imageTrackStack.shift();
11048 if (typeof url === 'undefined') {
11049 modules['showImages'].handleSRStyleToggleVisibility();
11052 if (BrowserDetect.isChrome()) {
11053 if (!chrome.extension.inIncognitoContext) {
11054 chrome.extension.sendMessage({
11055 requestType: 'addURLToHistory',
11059 modules['showImages'].imageTrackShift();
11060 } else if (BrowserDetect.isFirefox()) {
11061 // update: using XPCOM we may can add URLs to Firefox history without the iframe hack!
11063 requestType: 'addURLToHistory',
11066 self.postMessage(thisJSON);
11067 modules['showImages'].imageTrackShift();
11068 } else if (BrowserDetect.isOpera()) {
11070 requestType: 'addURLToHistory',
11073 opera.extension.postMessage(JSON.stringify(thisJSON));
11074 } else if (BrowserDetect.isSafari()) {
11076 requestType: 'addURLToHistory',
11079 safari.self.tab.dispatchMessage('addURLToHistory', thisJSON);
11080 } else if (typeof modules['showImages'].imageTrackFrame.contentWindow !== 'undefined') {
11081 modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
11083 modules['showImages'].imageTrackFrame.location.replace(url);
11087 //numbers just picked as sane initialization values
11089 diagonal: 0, //zero to represent the state where no the mouse button is not down
11092 getDragSize: function(e){
11093 var rc = e.target.getBoundingClientRect(),
11095 dragSize = p(p(e.clientX-rc.left, 2)+p(e.clientY-rc.top, 2), .5);
11097 return Math.round(dragSize);
11099 handleSRStyleToggleVisibility: function(image) {
11100 RESUtils.debounce('handleSRStyleToggleVisibility', 50, function() {
11101 var toggleEle = modules['styleTweaks'].styleToggleContainer;
11102 if (!toggleEle) return;
11103 var imageElems = image ? [ image ] : document.querySelectorAll('.RESImage');
11105 for (var i = 0 ; i < imageElems.length; i++) {
11106 var imageEle = imageElems[i];
11107 var imageID = imageEle.getAttribute('id');
11109 if (RESUtils.doElementsCollide(toggleEle, imageEle, 15)) {
11110 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'imageZoom-' + imageID);
11112 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'imageZoom-' + imageID);
11117 setPlaceholder: function(imageTag) {
11118 if (!($(imageTag).data('imagePlaceholder'))) {
11119 var thisPH = createElementWithID('div','RESImagePlaceholder');
11120 $(thisPH).addClass('RESImagePlaceholder');
11121 $(imageTag).data('imagePlaceholder', thisPH);
11122 // Add listeners for drag to resize functionality...
11123 $(imageTag).parent().append($(imageTag).data('imagePlaceholder'));
11125 $(imageTag).load(modules['showImages'].syncPlaceholder);
11127 syncPlaceholder: function(e) {
11128 var ele = e.target || e;
11129 var thisPH = $(ele).data('imagePlaceholder');
11130 $(thisPH).width($(ele).width() + 'px');
11131 $(thisPH).height($(ele).height() + 'px');
11132 $(ele).addClass('loaded');
11134 makeImageZoomable: function(imageTag) {
11135 if (this.options.imageZoom.value) {
11136 imageTag.addEventListener('mousedown', modules['showImages'].mousedownImage, false);
11137 imageTag.addEventListener('mouseup', modules['showImages'].dragImage, false);
11138 imageTag.addEventListener('mousemove', modules['showImages'].dragImage, false);
11139 imageTag.addEventListener('mouseout', modules['showImages'].mouseoutImage, false);
11140 imageTag.addEventListener('click', modules['showImages'].clickImage, false);
11143 mousedownImage: function(e) {
11144 if (e.button === 0) {
11145 if (!e.target.minWidth) e.target.minWidth = Math.max(1, Math.min(e.target.width, 100));
11146 modules['showImages'].dragTargetData.imageWidth = e.target.width;
11147 modules['showImages'].dragTargetData.diagonal = modules['showImages'].getDragSize(e);
11148 modules['showImages'].dragTargetData.dragging = false;
11149 modules['showImages'].dragTargetData.hasChangedWidth = false;
11150 e.preventDefault();
11153 mouseoutImage: function(e) {
11154 modules['showImages'].dragTargetData.diagonal = 0;
11156 dragImage: function(e) {
11157 if (modules['showImages'].dragTargetData.diagonal) {
11158 var newDiagonal = modules['showImages'].getDragSize(e),
11159 oldDiagonal = modules['showImages'].dragTargetData.diagonal,
11160 imageWidth = modules['showImages'].dragTargetData.imageWidth,
11161 maxWidth = Math.max(e.target.minWidth, newDiagonal/oldDiagonal*imageWidth);
11163 modules['showImages'].resizeImage(e.target, maxWidth);
11164 modules['showImages'].dragTargetData.dragging = true;
11166 modules['showImages'].handleSRStyleToggleVisibility(e.target);
11167 if (e.type === 'mouseup') {
11168 modules['showImages'].dragTargetData.diagonal = 0;
11171 clickImage: function(e) {
11172 modules['showImages'].dragTargetData.diagonal = 0;
11173 if (modules['showImages'].dragTargetData.hasChangedWidth) {
11174 modules['showImages'].dragTargetData.dragging = false;
11175 e.preventDefault();
11178 modules['showImages'].dragTargetData.hasChangedWidth = false;
11180 resizeImage: function(image, newWidth) {
11181 var currWidth = $(image).width();
11182 if (newWidth !== currWidth) {
11183 modules['showImages'].dragTargetData.hasChangedWidth = true;
11185 image.style.width=newWidth + 'px';
11186 image.style.maxWidth=newWidth + 'px';
11187 image.style.maxHeight='';
11188 image.style.height='auto';
11190 var thisPH = $(image).data('imagePlaceholder');
11191 $(thisPH).width($(image).width() + 'px');
11192 $(thisPH).height($(image).height() + 'px');
11197 acceptRegex: /^[^#]+?\.(gif|jpe?g|png)(?:[?&#_].*|$)/i,
11198 rejectRegex: /(wikipedia\.org\/wiki|photobucket\.com|gifsound\.com|\/wiki\/File:.*)/i,
11200 detect: function(elem) {
11201 var siteMod = modules['showImages'].siteModules['default'];
11202 var href = elem.href;
11203 return (siteMod.acceptRegex.test(href) && !siteMod.rejectRegex.test(href));
11205 handleLink: function(elem) {
11206 var def = $.Deferred();
11207 var href = elem.href;
11209 def.resolve(elem, {
11213 return def.promise()
11215 handleInfo: function(elem, info) {
11216 var def = $.Deferred();
11218 elem.type = info.type;
11219 elem.src = info.src;
11220 elem.href = info.src;
11222 if (RESUtils.pageType() === 'linklist') {
11223 $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
11227 return def.promise();
11231 APIKey: 'fe266bc9466fe69aa1cf0904e7298eda',
11232 // hashRe: /^https?:\/\/(?:i\.|edge\.|www\.)*imgur\.com\/(?:r\/[\w]+\/)?([\w]{5,}(?:[&,][\w]{5,})?)(\..+)?(?:#(\d*))?$/i,
11233 // the modified regex below fixes detection of "edited" imgur images, but imgur's edited images are broken right now actually, falling into
11234 // a redirect loop. preserving the old one just in case. however it also fixes detection of the extension (.jpg, for example) which
11235 // was too greedy a search...
11236 // the hashRe below was provided directly by MrGrim (well, everything after the domain was), using that now.
11237 hashRe: /^https?:\/\/(?:i\.|m\.|edge\.|www\.)*imgur\.com\/(?!gallery)(?!removalrequest)(?!random)(?!memegen)([A-Za-z0-9]{5}|[A-Za-z0-9]{7})[sbtmlh]?(\.(?:jpe?g|gif|png))?(\?.*)?$/i,
11238 albumHashRe: /^https?:\/\/(?:i\.|m\.)?imgur\.com\/(?:a|gallery)\/([\w]+)(\..+)?(?:\/)?(?:#\w*)?$/i,
11239 apiPrefix: 'http://api.imgur.com/2/',
11242 detect: function(elem) {
11243 return elem.href.toLowerCase().indexOf('imgur.com/') !== -1;
11245 handleLink: function(elem) {
11246 var siteMod = modules['showImages'].siteModules['imgur'];
11247 var def = $.Deferred();
11248 var href = elem.href.split('?')[0];
11249 var groups = siteMod.hashRe.exec(href);
11250 if (!groups) var albumGroups = siteMod.albumHashRe.exec(href);
11251 if (groups && !groups[2]) {
11252 if (groups[1].search(/[&,]/) > -1) {
11253 var hashes = groups[1].split(/[&,]/);
11254 def.resolve(elem, {
11255 album: {images: hashes.map(function(hash) {
11257 image: {title: '', caption: '', hash: hash},
11258 links: {original: 'http://i.imgur.com/'+hash+'.jpg'}
11263 // removed caption API calls as they don't seem to exist/matter for single images, only albums...
11264 //If we don't show captions, then we can skip the API call.
11265 def.resolve(elem, {image: {
11267 //Imgur doesn't really care about the extension and the browsers don't seem to either.
11268 original: 'http://i.imgur.com/'+groups[1]+'.jpg'
11272 } else if (albumGroups && !albumGroups[2]) {
11273 var apiURL = siteMod.apiPrefix + 'album/' + albumGroups[1] + '.json';
11274 elem.imgHash = albumGroups[1];
11275 if (apiURL in siteMod.calls) {
11276 if (siteMod.calls[apiURL] != null) {
11277 def.resolve(elem, siteMod.calls[apiURL]);
11282 GM_xmlhttpRequest({
11285 // aggressiveCache: true,
11286 onload: function(response) {
11288 var json = JSON.parse(response.responseText);
11289 siteMod.calls[apiURL] = json;
11290 def.resolve(elem, json);
11292 siteMod.calls[apiURL] = null;
11296 onerror: function(response) {
11304 return def.promise();
11306 handleInfo: function(elem, info) {
11307 if ('image' in info) {
11308 return modules['showImages'].siteModules['imgur'].handleSingleImage(elem, info);
11309 } else if ('album' in info) {
11310 return modules['showImages'].siteModules['imgur'].handleGallery(elem, info);
11311 } else if (info.error && info.error.message === 'Album not found') {
11312 // This case comes up when there is an imgur.com/gallery/HASH link that
11313 // links to an image, not an album (not to be confused with the word "gallery", ugh)
11317 original: 'http://i.imgur.com/' + elem.imgHash + '.jpg'
11322 return modules['showImages'].siteModules['imgur'].handleSingleImage(elem, info);
11324 return $.Deferred().reject().promise();
11325 // console.log("ERROR", info);
11326 // console.log(arguments.callee.caller);
11329 handleSingleImage: function(elem, info) {
11330 elem.src = info.image.links.original;
11331 elem.href = info.image.links.original;
11332 if (RESUtils.pageType() === 'linklist') {
11333 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11335 elem.type = 'IMAGE';
11336 if (info.image.image.caption) elem.caption = info.image.image.caption;
11337 return $.Deferred().resolve(elem).promise();
11339 handleGallery: function(elem, info) {
11340 var base = elem.href.split('#')[0];
11341 elem.src = info.album.images.map(function(e, i, a) {
11343 title: e.image.title,
11344 src: e.links.original,
11345 href: base + '#' + e.image.hash,
11346 caption: e.image.caption
11350 var hash = elem.hash.slice(1);
11352 for (var i = 0; i < elem.src.length; i++) {
11353 if (hash == info.album.images[i].image.hash) {
11354 elem.galleryStart = i;
11359 elem.galleryStart = parseInt(hash, 10);
11362 elem.imageTitle = info.album.title;
11363 elem.caption = info.album.description;
11364 elem.type = 'GALLERY';
11365 return $.Deferred().resolve(elem).promise();
11372 detect: function(elem) {
11373 var href = elem.href.toLowerCase();
11374 return href.indexOf('gfycat.com') !== -1 && href.substring(-1) !== '+';
11376 handleLink: function(elem) {
11377 var hashRe = /^http:\/\/[a-zA-Z0-9\-\.]*gfycat\.com\/(\w+)\.?/i;
11378 var def = $.Deferred();
11379 var groups = hashRe.exec(elem.href);
11381 if (!groups) return def.reject();
11382 var href = elem.href.toLowerCase();
11383 var hotLink = false;
11384 if(href.indexOf('giant.gfycat') !== -1 ||
11385 href.indexOf('fat.gfycat') !== -1 ||
11386 href.indexOf('zippy.gfycat') !== -1)
11389 var siteMod = modules['showImages'].siteModules['gfycat'];
11390 var apiURL = 'http://gfycat.com/cajax/get/' + groups[1];
11392 if (apiURL in siteMod.calls) {
11393 if (siteMod.calls[apiURL] != null) {
11394 def.resolve(elem, siteMod.calls[apiURL]);
11396 siteMod.calls[apiURL] = null;
11400 GM_xmlhttpRequest({
11403 aggressiveCache: true,
11404 onload: function(response) {
11406 var json = JSON.parse(response.responseText);
11407 json.gfyItem.src = elem.href;
11408 json.gfyItem.hotLink = hotLink
11409 siteMod.calls[apiURL] = json;
11410 def.resolve(elem, json.gfyItem);
11412 siteMod.calls[apiURL] = null;
11416 onerror: function(response) {
11421 return def.promise();
11423 handleInfo: function(elem, info) {
11424 function humanSize(bytes) {
11425 var byteUnits = [' kB', ' MB'];
11426 for(var i=-1; bytes > 1024; i++) {
11427 bytes = bytes / 1024;
11429 return Math.max(bytes, 0.1).toFixed(1) + byteUnits[i];
11433 elem.src = info.src;
11434 elem.imageTitle = humanSize(info.gifSize);
11435 if(((info.gifSize > 524288 && (info.gifSize / info.mp4Size) > 5)
11436 || (info.gifSize > 1048576 && (info.gifSize / info.mp4Size) > 2))
11437 && document.createElement('video').canPlayType)
11438 elem.imageTitle += ' (' + humanSize(info.mp4Size) + " for <a href='http://gfycat.com/" + info.gfyName + "'>HTML5 version.</a>)";
11440 if (RESUtils.pageType() === 'linklist') {
11441 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11443 return $.Deferred().resolve(elem).promise();
11446 elem.mediaOptions = {
11452 sources[0] = {'file': info.mp4Url,'type':'video/mp4'};
11453 sources[1] = {'file': info.webmUrl,'type':'video/webm'};
11454 elem.type = 'VIDEO';
11455 $(elem).data('sources', sources);
11457 if (RESUtils.pageType() === 'linklist') {
11458 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11461 return $.Deferred().resolve(elem).promise();
11466 detect: function(elem) {
11467 var href = elem.href.toLowerCase();
11468 return href.indexOf('eho.st') !== -1 && href.substring(-1) !== '+';
11470 handleLink: function(elem) {
11471 var hashRe = /^http:\/\/(?:i\.)?(?:\d+\.)?eho\.st\/(\w+)\/?/i;
11472 var def = $.Deferred();
11473 var groups = hashRe.exec(elem.href);
11475 def.resolve(elem, {
11476 src: 'http://i.eho.st/'+groups[1]+'.jpg'
11481 return def.promise();
11483 handleInfo: function(elem, info) {
11484 elem.type = 'IMAGE';
11485 elem.src = info.src;
11486 elem.href = info.src;
11487 if (RESUtils.pageType() === 'linklist') {
11488 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11490 elem.onerror = function() {
11491 if (this.src.match(/\.jpg/)) {
11492 this.src = this.src.slice(0, elem.src.length - 3) + 'png';
11493 } else if (this.src.match(/\.png/)) {
11494 this.src = this.src.slice(0, elem.src.length - 3) + 'gif';
11497 return $.Deferred().resolve(elem).promise();
11502 detect: function(elem) {
11503 var href = elem.href.toLowerCase();
11504 return href.indexOf('picsarus.com') !== -1 && href.substring(-1) !== '+';
11506 handleLink: function(elem) {
11507 var hashRe = /^https?:\/\/(?:i\.|edge\.|www\.)*picsarus\.com\/(?:r\/[\w]+\/)?([\w]{6,})(\..+)?$/i;
11508 var def = $.Deferred();
11509 var groups = hashRe.exec(elem.href);
11511 def.resolve(elem, {
11512 src: 'http://www.picsarus.com/'+groups[1]+'.jpg'
11517 return def.promise();
11519 handleInfo: function(elem, info) {
11520 elem.type = 'IMAGE';
11521 elem.src = info.src;
11522 elem.href = info.src;
11523 if (RESUtils.pageType() === 'linklist') {
11524 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11526 return $.Deferred().resolve(elem).promise();
11531 detect: function(elem) {
11532 return elem.href.toLowerCase().indexOf('snag.gy/') !== -1;
11534 handleLink: function(elem) {
11535 var def = $.Deferred();
11536 var href = elem.href;
11537 var extensions = ['.jpg','.png','.gif'];
11538 if (href.indexOf('i.snag') === -1) href = href.replace('snag.gy', 'i.snag.gy');
11539 if (extensions.indexOf(href.substr(-4)) === -1) href = href+'.jpg';
11540 def.resolve(elem, {src: href});
11541 return def.promise();
11543 handleInfo: function(elem, info) {
11544 elem.type = 'IMAGE';
11545 elem.src = info.src;
11546 elem.href = info.src;
11547 if (RESUtils.pageType() === 'linklist') {
11548 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11550 return $.Deferred().resolve(elem).promise();
11555 detect: function(elem) {
11556 var href = elem.href.toLowerCase();
11557 return href.indexOf('picshd.com/') !== -1;
11559 handleLink: function(elem) {
11560 var def = $.Deferred();
11561 var hashRe = /^https?:\/\/(?:i\.|edge\.|www\.)*picshd\.com\/([\w]{5,})(\..+)?$/i;
11562 var groups = hashRe.exec(elem.href);
11564 def.resolve(elem, 'http://i.picshd.com/'+groups[1]+'.jpg');
11568 return def.promise();
11570 handleInfo: function(elem, info) {
11571 elem.type = 'IMAGE';
11574 if (RESUtils.pageType() === 'linklist') {
11575 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11577 return $.Deferred().resolve(elem).promise();
11583 detect: function(elem) {
11584 var href = elem.href.toLowerCase();
11585 return href.indexOf('min.us') !== -1 && href.indexOf('blog.') === -1;
11587 handleLink: function(elem) {
11588 var def = $.Deferred();
11589 var hashRe = /^http:\/\/min\.us\/([\w]+)(?:#[\d+])?$/i;
11590 var href = elem.href.split('?')[0];
11591 //TODO: just make default run first and remove this
11592 var getExt = href.split('.');
11593 var ext = (getExt.length > 1?getExt[getExt.length - 1].toLowerCase():'');
11594 if (['jpg', 'jpeg', 'png', 'gif'].indexOf(ext) !== -1) {
11595 var groups = hashRe.exec(href);
11596 if (groups && !groups[2]) {
11597 var hash = groups[1];
11598 if (hash.substr(0, 1) === 'm') {
11599 var apiURL = 'http://min.us/api/GetItems/' + hash;
11600 var calls = modules['showImages'].siteModules['minus'].calls;
11601 if (apiURL in calls) {
11602 if (calls[apiURL] != null) {
11603 def.resolve(elem, calls[apiURL]);
11608 GM_xmlhttpRequest({
11611 onload: function(response) {
11613 var json = JSON.parse(response.responseText);
11614 modules['showImages'].siteModules['minus'].calls[apiURL] = json;
11615 def.resolve(elem, json);
11617 modules['showImages'].siteModules['minus'].calls[apiURL] = null;
11621 onerror: function(response) {
11626 } else { // if not 'm', not a gallery, we can't do anything with the API.
11635 return def.promise();
11637 handleInfo: function(elem, info) {
11638 var def = $.Deferred();
11639 //TODO: Handle titles
11640 //TODO: Handle possibility of flash items
11641 if ('ITEMS_GALLERY' in info) {
11642 if (info.ITEMS_GALLERY.length > 1) {
11643 elem.type = 'GALLERY';
11645 src: info.ITEMS_GALLERY
11648 elem.type = 'IMAGE';
11649 elem.href = info.ITEMS_GALLERY[0];
11650 if (RESUtils.pageType() === 'linklist') {
11651 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11653 elem.src = info.ITEMS_GALLERY[0];
11659 return def.promise()
11664 detect: function(elem) {
11665 var hashRe = /^http:\/\/(?:\w+)\.?flickr\.com\/(?:.*)\/([\d]{10})\/?(?:.*)?$/i;
11666 var href = elem.href;
11667 return hashRe.test(href);
11669 handleLink: function(elem) {
11670 var def = $.Deferred();
11671 // modules['showImages'].createImageExpando(elem);
11672 // var selector = '#allsizes-photo > IMG';
11673 var href = elem.href;
11674 if (href.indexOf('/sizes') === -1) {
11675 var inPosition = href.indexOf('/in/');
11676 var inFragment = '';
11677 if (inPosition !== -1) {
11678 inFragment = href.substring(inPosition);
11679 href = href.substring(0, inPosition);
11682 href += '/sizes/c' + inFragment;
11684 href = href.replace('/lightbox', '');
11685 href = 'http://www.flickr.com/services/oembed/?format=json&url=' + href;
11686 GM_xmlhttpRequest({
11689 onload: function(response) {
11691 var json = JSON.parse(response.responseText);
11692 def.resolve(elem, json);
11697 onerror: function(response) {
11701 return def.promise();
11703 handleInfo: function(elem, info) {
11704 var def = $.Deferred();
11705 var imgRe = /\.(jpg|jpeg|gif|png)/i;
11706 if ('url' in info) {
11707 elem.imageTitle = info.title;
11708 var original_url = elem.href;
11709 if(imgRe.test(info.url)) {
11710 elem.src = info.url;
11711 // elem.href = info.url;
11713 elem.src = info.thumbnail_url;
11714 // elem.href = info.thumbnail_url;
11716 if (RESUtils.pageType() === 'linklist') {
11717 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11719 elem.credits = 'Picture by: <a href="'+info.author_url+'">'+info.author_name+'</a> @ Flickr';
11720 elem.type = 'IMAGE';
11725 return def.promise();
11730 detect: function(elem) {
11731 return elem.href.toLowerCase().indexOf('cloud.steampowered.com') !== -1;
11733 handleLink: function(elem) {
11734 return $.Deferred().resolve(elem, elem.href).promise();
11736 handleInfo: function(elem, info) {
11737 elem.type = 'IMAGE';
11740 if (RESUtils.pageType() === 'linklist') {
11741 $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
11743 return $.Deferred().resolve(elem).promise();
11748 matchRe: /^http:\/\/(?:fav\.me\/.*|(?:.+\.)?deviantart\.com\/(?:art\/.*|[^#]*#\/d.*))$/i,
11750 detect: function(elem) {
11751 return modules['showImages'].siteModules['deviantart'].matchRe.test(elem.href);
11753 handleLink: function(elem) {
11754 var def = $.Deferred();
11755 var siteMod = modules['showImages'].siteModules['deviantart'];
11756 var apiURL = 'http://backend.deviantart.com/oembed?url=' + encodeURIComponent(elem.href);
11757 if (apiURL in siteMod.calls) {
11758 if (siteMod.calls[apiURL] != null) {
11759 def.resolve(elem, siteMod.calls[apiURL]);
11764 GM_xmlhttpRequest({
11767 // aggressiveCache: true,
11768 onload: function(response) {
11770 var json = JSON.parse(response.responseText);
11771 siteMod.calls[apiURL] = json;
11772 def.resolve(elem, json);
11774 siteMod.calls[apiURL] = null;
11778 onerror: function(response) {
11783 return def.promise();
11785 handleInfo: function(elem, info) {
11786 var def = $.Deferred();
11787 if ('url' in info) {
11788 elem.imageTitle = info.title;
11789 var original_url = elem.href;
11790 if(['jpg', 'jpeg', 'png', 'gif'].indexOf(info.url) !== -1) {
11791 elem.src = info.url;
11792 // elem.href = info.url;
11794 elem.src = info.thumbnail_url;
11795 // elem.href = info.thumbnail_url;
11797 if (RESUtils.pageType() === 'linklist') {
11798 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11800 // elem.credits = 'Original link: <a href="'+original_url+'">'+original_url+'</a><br>Art by: <a href="'+info.author_url+'">'+info.author_name+'</a> @ deviantART';
11801 elem.credits = 'Art by: <a href="'+info.author_url+'">'+info.author_name+'</a> @ deviantART';
11802 elem.type = 'IMAGE';
11807 return def.promise();
11812 APIKey: 'WeJQquHCAasi5EzaN9jMtIZkYzGfESUtEvcYDeSMLICveo3XDq',
11813 matchRE: /^https?:\/\/([a-z0-9\-]+\.tumblr\.com)\/post\/(\d+)(?:\/.*)?$/i,
11814 go: function() { },
11815 detect: function(elem) {
11816 return modules['showImages'].siteModules['tumblr'].matchRE.test(elem.href);
11818 handleLink: function(elem) {
11819 var def = $.Deferred();
11820 var siteMod = modules['showImages'].siteModules['tumblr'];
11821 var groups = siteMod.matchRE.exec(elem.href);
11823 var apiURL = 'http://api.tumblr.com/v2/blog/'+groups[1]+'/posts?api_key='+siteMod.APIKey+'&id='+groups[2] + '&filter=raw';
11824 if (apiURL in siteMod.calls) {
11825 if (siteMod.calls[apiURL] != null) {
11826 def.resolve(elem, siteMod.calls[apiURL]);
11831 GM_xmlhttpRequest({
11834 // aggressiveCache: true,
11835 onload: function(response) {
11837 var json = JSON.parse(response.responseText);
11838 if ('meta' in json && json.meta.status === 200) {
11839 siteMod.calls[apiURL] = json;
11840 def.resolve(elem, json);
11842 siteMod.calls[apiURL] = null;
11846 siteMod.calls[apiURL] = null;
11850 onerror: function(response) {
11858 return def.promise();
11860 handleInfo: function(elem, info) {
11861 var def = $.Deferred();
11862 var original_url = elem.href;
11863 var post = info.response.posts[0];
11864 switch (post.type) {
11866 if (post.photos.length > 1) {
11867 elem.type = 'GALLERY';
11868 elem.src = post.photos.map(function(e) {
11870 src: e.original_size.url,
11875 elem.type = "IMAGE";
11876 elem.src = post.photos[0].original_size.url;
11880 elem.type = 'TEXT';
11881 elem.imageTitle = post.title;
11882 if (post.format === 'markdown') {
11883 elem.src = modules['commentPreview'].converter.render(post.body)
11884 } else if (post.format === 'html') {
11885 elem.src = post.body;
11889 return def.reject().promise();
11892 elem.caption = post.caption;
11893 if (RESUtils.pageType() === 'linklist') {
11894 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11896 elem.credits = 'Posted by: <a href="'+info.response.blog.url+'">'+info.response.blog.name+'</a> @ Tumblr';
11898 return def.promise()
11903 detect: function(elem) {
11904 return elem.href.toLowerCase().indexOf('memecrunch.com') !== -1;
11906 handleLink: function(elem) {
11907 var def = $.Deferred();
11908 var hashRe = /^http:\/\/memecrunch\.com\/meme\/([0-9A-Z]+)\/([\w\-]+)(\/image\.(png|jpg))?/i;
11909 var groups = hashRe.exec(elem.href);
11910 if (groups && typeof groups[1] !== 'undefined') {
11911 def.resolve(elem, 'http://memecrunch.com/meme/'+groups[1]+'/'+(groups[2]||'null')+'/image.png');
11915 return def.promise();
11917 handleInfo: function(elem, info) {
11918 elem.type = 'IMAGE';
11921 if (RESUtils.pageType() === 'linklist') {
11922 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11924 modules['showImages'].createImageExpando(elem);
11930 detect: function(elem) {
11931 return elem.href.toLowerCase().indexOf('mediacru.sh') !== -1;
11933 handleLink: function(elem) {
11934 var hashRe = /^https?:\/\/(?:www\.)?mediacru\.sh\/([\w-]+)$/i;
11935 var def = $.Deferred();
11936 var groups = hashRe.exec(elem.href);
11937 if (!groups) return def.reject();
11938 var siteMod = modules['showImages'].siteModules['mediacrush'];
11939 var apiURL = 'https://mediacru.sh/' + encodeURIComponent(groups[1]) + '.json';
11940 if (apiURL in siteMod.calls) {
11941 if (siteMod.calls[apiURL] != null) {
11942 def.resolve(elem, siteMod.calls[apiURL]);
11944 siteMod.calls[apiURL] = null;
11948 GM_xmlhttpRequest({
11951 // aggressiveCache: true,
11952 onload: function(response) {
11954 var json = JSON.parse(response.responseText);
11955 siteMod.calls[apiURL] = json;
11956 def.resolve(elem, json);
11958 siteMod.calls[apiURL] = null;
11962 onerror: function(response) {
11967 return def.promise();
11969 handleInfo: function(elem, info) {
11970 var def = $.Deferred()
11971 // check files to see if video or image
11972 elem.type = 'IMAGE';
11974 src: 'http://mediacru.sh/'+info.original,
11977 if (info.original.indexOf('.gif') !== -1) {
11978 elem.mediaOptions = {
11984 for (var i=0, len=info.files.length; i<len; i++) {
11985 if (info.files[i].type.indexOf('video') !== -1) {
11986 elem.type = 'VIDEO';
11987 info.files[i].file = 'http://mediacru.sh/' + info.files[i].file;
11988 mediaData.sources.push(info.files[i]);
11990 if (info.files[i].type.indexOf('audio') !== -1) {
11991 elem.type = 'AUDIO';
11992 info.files[i].file = 'http://mediacru.sh/' + info.files[i].file;
11993 mediaData.sources.push(info.files[i]);
11996 if (elem.type === 'IMAGE') {
11997 elem.src = mediaData.src;
11999 $(elem).data('sources', mediaData.sources);
12000 return $.Deferred().resolve(elem).promise();
12007 urlMod: function(url) {
12008 return url+'/embed/postcard';
12011 detect: function(elem) {
12012 return elem.href.toLowerCase().indexOf('vine.co') !== -1;
12014 handleLink: function(elem) {
12015 var hashRe = /^https?:\/\/(?:www\.)?vine\.co\/v\/(\w{10,12})$/i;
12016 var def = $.Deferred();
12017 var groups = hashRe.exec(elem.href);
12019 def.resolve(elem, {
12025 return def.promise();
12027 handleInfo: function(elem, info) {
12028 elem.type = 'NOEMBED';
12029 elem.src = info.src;
12030 elem.expandoClass = 'collapsed video';
12031 elem.siteMod = modules['showImages'].siteModules['vine'];
12032 return $.Deferred().resolve(elem).promise();
12036 go: function() { },
12037 detect: function(elem) {
12038 return elem.href.toLowerCase().indexOf('livememe.com') !== -1;
12040 handleLink: function(elem) {
12041 var def = $.Deferred();
12042 var hashRe = /^http:\/\/(?:www\.livememe\.com|lvme\.me)\/(?!edit)([\w]+)\/?/i;
12043 var groups = hashRe.exec(elem.href);
12045 def.resolve(elem, 'http://www.livememe.com/'+groups[1]+'.jpg');
12049 return def.promise();
12051 handleInfo: function(elem, info) {
12052 elem.type = 'IMAGE';
12055 if (RESUtils.pageType() === 'linklist') {
12056 $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
12058 return $.Deferred().resolve(elem).promise();
12063 detect: function(elem) {
12064 return elem.href.toLowerCase().indexOf('makeameme.org') !== -1;
12066 handleLink: function(elem) {
12067 var def = $.Deferred()
12068 var hashRe = /^http:\/\/makeameme\.org\/meme\/([\w-]+)\/?/i;
12069 var groups = hashRe.exec(elem.href);
12071 def.resolve(elem, 'http://makeameme.org/media/created/'+groups[1]+'.jpg');
12075 return def.promise();
12077 handleInfo: function(elem, info) {
12078 elem.type = 'IMAGE';
12081 if (RESUtils.pageType() === 'linklist') {
12082 $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
12084 return $.Deferred().resolve(elem).promise();
12089 detect: function(elem) {
12090 return elem.href.toLowerCase().indexOf('memefive.com') !== -1;
12092 handleLink: function(elem) {
12093 var def = $.Deferred()
12094 var hashRe = /^http:\/\/(?:www\.)?(?:memefive\.com)\/meme\/([\w]+)\/?/i;
12095 var altHashRe = /^http:\/\/(?:www\.)?(?:memefive\.com)\/([\w]+)\/?/i;
12096 var groups = hashRe.exec(elem.href);
12098 groups = altHashRe.exec(elem.href);
12101 def.resolve(elem, 'http://memefive.com/memes/'+groups[1]+'.jpg');
12105 return def.promise();
12107 handleInfo: function(elem, info) {
12108 elem.type = 'IMAGE';
12111 if (RESUtils.pageType() === 'linklist') {
12112 $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
12114 return $.Deferred().resolve(elem).promise();
12118 go: function() { },
12119 detect: function(elem) {
12120 return elem.href.toLowerCase().indexOf('.memegen.') !== -1;
12122 handleLink: function(elem) {
12123 var def = $.Deferred();
12124 var hashRe = /^http:\/\/((?:www|ar|ru|id|el|pt|tr)\.memegen\.(?:com|de|nl|fr|it|es|se|pl))(\/a)?\/(?:meme|mem|mim)\/([A-Za-z0-9]+)\/?/i;
12125 var groups = hashRe.exec(elem.href);
12127 // Animated vs static meme images.
12129 def.resolve(elem, 'http://a.memegen.com/' + groups[3] + '.gif');
12131 def.resolve(elem, 'http://m.memegen.com/' + groups[3] + '.jpg');
12136 return def.promise();
12138 handleInfo: function(elem, info) {
12139 elem.type = 'IMAGE';
12142 if (RESUtils.pageType() === 'linklist') {
12143 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
12145 return $.Deferred().resolve(elem).promise();
12151 modules['showKarma'] = {
12152 moduleID: 'showKarma',
12153 moduleName: 'Show Comment Karma',
12154 category: 'Accounts',
12159 description: 'Separator character between post/comment karma'
12164 description: 'Use commas for large karma numbers'
12167 description: 'Shows your comment karma next to your link karma.',
12168 isEnabled: function() {
12169 return RESConsole.getModulePrefs(this.moduleID);
12172 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
12174 isMatchURL: function() {
12175 return RESUtils.isMatchURL(this.moduleID);
12178 if ((this.isEnabled()) && (this.isMatchURL())) {
12179 if (RESUtils.loggedInUser()) {
12180 RESUtils.getUserInfo(modules['showKarma'].updateKarmaDiv);
12184 updateKarmaDiv: function(userInfo) {
12185 var karmaDiv = document.querySelector("#header-bottom-right .userkarma");
12186 if ((typeof karmaDiv !== 'undefined') && (karmaDiv !== null)) {
12187 var linkKarma = karmaDiv.innerHTML;
12188 karmaDiv.title = '';
12189 var commentKarma = userInfo.data.comment_karma;
12190 if (modules['showKarma'].options.useCommas.value) {
12191 linkKarma = RESUtils.addCommas(linkKarma);
12192 commentKarma = RESUtils.addCommas(commentKarma);
12194 $(karmaDiv).html("<a title=\"link karma\" href=\"/user/" + RESUtils.loggedInUser() + "/submitted/\">" + linkKarma + "</a> " + modules['showKarma'].options.separator.value + " <a title=\"comment karma\" href=\"/user/" + RESUtils.loggedInUser() + "/comments/\">" + commentKarma + "</a>");
12199 modules['hideChildComments'] = {
12200 moduleID: 'hideChildComments',
12201 moduleName: 'Hide All Child Comments',
12202 category: 'Comments',
12204 // any configurable options you have go here...
12205 // options must have a type and a value..
12206 // valid types are: text, boolean (if boolean, value must be true or false)
12211 description: 'Automatically hide all but parent comments, or provide a link to hide them all?'
12214 description: 'Allows you to hide all comments except for replies to the OP for easier reading.',
12215 isEnabled: function() {
12216 return RESConsole.getModulePrefs(this.moduleID);
12219 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
12220 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
12222 isMatchURL: function() {
12223 return RESUtils.isMatchURL(this.moduleID);
12226 if ((this.isEnabled()) && (this.isMatchURL())) {
12227 var toggleButton = document.createElement('li');
12228 this.toggleAllLink = document.createElement('a');
12229 this.toggleAllLink.textContent = 'hide all child comments';
12230 this.toggleAllLink.setAttribute('action','hide');
12231 this.toggleAllLink.setAttribute('href','javascript:void(0);');
12232 this.toggleAllLink.setAttribute('title','Show only replies to original poster.');
12233 this.toggleAllLink.addEventListener('click', function() {
12234 modules['hideChildComments'].toggleComments(this.getAttribute('action'));
12235 if (this.getAttribute('action') === 'hide') {
12236 this.setAttribute('action','show');
12237 this.setAttribute('title','Show all comments.');
12238 this.textContent = 'show all child comments';
12240 this.setAttribute('action','hide');
12241 this.setAttribute('title','Show only replies to original poster.');
12242 this.textContent = 'hide all child comments';
12245 toggleButton.appendChild(this.toggleAllLink);
12246 var commentMenu = document.querySelector('ul.buttons');
12248 commentMenu.appendChild(toggleButton);
12249 var rootComments = document.querySelectorAll('div.commentarea > div.sitetable > div.thing > div.child > div.listing');
12250 for (var i=0, len=rootComments.length; i<len; i++) {
12251 var toggleButton = document.createElement('li');
12252 var toggleLink = document.createElement('a');
12253 toggleLink.textContent = 'hide child comments';
12254 toggleLink.setAttribute('action','hide');
12255 toggleLink.setAttribute('href','javascript:void(0);');
12256 toggleLink.setAttribute('class','toggleChildren');
12257 // toggleLink.setAttribute('title','Hide child comments.');
12258 toggleLink.addEventListener('click', function(e) {
12259 modules['hideChildComments'].toggleComments(this.getAttribute('action'), this);
12260 if (this.getAttribute('action') === 'hide') {
12261 this.setAttribute('action','show');
12262 // this.setAttribute('title','show child comments.');
12263 this.textContent = 'show child comments';
12265 this.setAttribute('action','hide');
12266 // this.setAttribute('title','hide child comments.');
12267 this.textContent = 'hide child comments';
12270 toggleButton.appendChild(toggleLink);
12271 var sib = rootComments[i].parentNode.previousSibling;
12272 if (typeof sib !== 'undefined') {
12273 var sibMenu = sib.querySelector('ul.buttons');
12274 if (sibMenu) sibMenu.appendChild(toggleButton);
12277 if (this.options.automatic.value) {
12278 RESUtils.click(this.toggleAllLink);
12283 toggleComments: function(action, obj) {
12285 var thisChildren = $(obj).closest('.thing').children('.child').children('.sitetable')[0];
12286 thisChildren.style.display = (action === 'hide') ? 'none' : 'block';
12288 // toggle all comments
12289 var commentContainers = document.querySelectorAll('div.commentarea > div.sitetable > div.thing');
12290 for (var i=0, len=commentContainers.length; i<len; i++) {
12291 var thisChildren = commentContainers[i].querySelector('div.child > div.sitetable');
12292 var thisToggleLink = commentContainers[i].querySelector('a.toggleChildren');
12293 if (thisToggleLink !== null) {
12294 if (action === 'hide') {
12295 if (thisChildren !== null) {
12296 thisChildren.style.display = 'none'
12298 thisToggleLink.textContent = 'show child comments';
12299 // thisToggleLink.setAttribute('title','show child comments');
12300 thisToggleLink.setAttribute('action','show');
12302 if (thisChildren !== null) {
12303 thisChildren.style.display = 'block';
12305 thisToggleLink.textContent = 'hide child comments';
12306 // thisToggleLink.setAttribute('title','hide child comments');
12307 thisToggleLink.setAttribute('action','hide');
12315 modules['showParent'] = {
12316 moduleID: 'showParent',
12317 moduleName: 'Show Parent on Hover',
12318 category: 'Comments',
12323 description: 'Delay, in milliseconds, before parent hover loads. Default is 500.'
12328 description: 'Delay, in milliseconds, before parent hover fades away after the mouse leaves. Default is 200.'
12333 description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
12339 { name: 'Up', value: 'up' },
12340 { name: 'Down', value: 'down' }
12342 description: 'Order the parent comments to place the closest parent at the top (down) or at the bottom (up).'
12345 description: 'Shows the parent comments when hovering over the "parent" link of a comment.',
12346 isEnabled: function() {
12347 return RESConsole.getModulePrefs(this.moduleID);
12350 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
12351 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
12353 isMatchURL: function() {
12354 return RESUtils.isMatchURL(this.moduleID);
12357 if ((this.isEnabled()) && (this.isMatchURL())) {
12358 if (modules['showParent'].options.direction.value === 'up') {
12359 document.html.classList.add('res-parents-up');
12361 document.html.classList.add('res-parents-down');
12363 $('body').on('mouseenter', '.comment .buttons :not(:first-child) .bylink', function(e) {
12364 RESUtils.hover.begin(this, {
12365 openDelay: modules['showParent'].options.hoverDelay.value,
12366 fadeDelay: modules['showParent'].options.fadeDelay.value,
12367 fadeSpeed: modules['showParent'].options.fadeSpeed.value
12368 }, modules['showParent'].showCommentHover , {});
12371 $('body').on('click', '#RESHoverContainer .parentCommentWrapper .arrow', modules['showParent'].handleVoteClick);
12374 handleVoteClick: function(evt) {
12375 var voteKey = {'-1':'disliked', 0:'unvoted', 1:'liked'};
12377 var id = $(this).parent().parent().attr('data-fullname');
12379 var direction = /(up|down)(mod)?/.exec(this.className);
12380 if (direction) direction = direction[1];
12383 var targetButton = $('.content .thing.id-'+id+' > .midcol').find('.arrow.'+direction + ', .arrow.'+direction+'mod');
12384 if (targetButton.length !== 1) {
12385 console.error("When attempting to find %s arrow for comment %s %d elements were returned", direction, id, targetButton.length);
12388 targetButton.click();
12390 var clickedDir = (direction==='up'?1:-1);
12392 var mid = $(this).parent();
12393 if (mid.hasClass('unvoted')) startDir = 0;
12394 else if (mid.hasClass('likes')) startDir = 1;
12395 else if (mid.hasClass('dislikes')) startDir = -1;
12398 var newDir = clickedDir===startDir ? 0 : clickedDir;
12401 mid.parent().children('.'+voteKey[startDir]).removeClass(voteKey[startDir]).addClass(voteKey[newDir])
12402 mid.find('.up, .upmod')
12403 .toggleClass('upmod', clickedDir === 1)
12404 .toggleClass('up', clickedDir !== 1);
12405 mid.find('.down, .downmod')
12406 .toggleClass('downmod', clickedDir === -1)
12407 .toggleClass('down', clickedDir !== -1);
12409 showCommentHover: function(def, base, context) {
12410 var direction = modules['showParent'].options.direction.value;
12411 var thing = $(base);
12413 //If the passed element is not a `.thing` move up to the nearest `.thing`
12414 if (!$(thing).is('.thing')) thing = thing.parents('.thing:first');
12415 var parents = $(thing).parents('.thing').clone();
12417 if (direction === 'up') {
12418 parents = $(parents.get().reverse());
12421 parents.addClass('comment parentComment').removeClass('thing even odd');
12422 parents.children('.child').remove(); // replies and reply edit form
12423 parents.each(function(i) {
12424 //A link to go to the actual comment
12425 var id = $(this).attr('data-fullname');
12426 if (id != undefined) {
12428 $(this).find('> .entry .tagline').append('<a class="bylink parentlink" href="#'+id+'"">goto comment</a>');
12431 parents.find('.parent').remove();
12432 parents.find('.usertext-body').show(); // contents
12433 parents.find('.flat-list.buttons').remove(); // buttons
12434 parents.find('.usertext-edit').remove(); // edit form
12435 parents.find('.RESUserTag').remove(); // tags
12436 parents.find('.voteWeight').remove(); // tags
12437 parents.find('.entry').removeClass('RES-keyNav-activeElement');
12438 parents.find('.author.userTagged').removeClass('userTagged'); //tags again
12439 parents.find('.collapsed').remove(); //unused collapse view
12440 parents.find('.expand').remove(); //expand vutton
12441 parents.find('form').attr('id', ''); //ID's should be unique
12442 parents.find('.arrow').attr('onclick', ''); //clear the vote handlers
12444 I am stripping out the image viewer stuff for now.
12445 Making the image viewer work here requires some changes that are for another time.
12447 parents.find('.madeVisible, .toggleImage').remove(); //image viewer
12448 parents.find('.keyNavAnnotation').remove();
12450 var container = $('<div class="parentCommentWrapper">');
12451 container.append(parents);
12452 $(parents).slice(0,-1).after('<div class="parentArrow">reply to</div>');
12453 def.resolve("Parents", container);
12457 modules['neverEndingReddit'] = {
12458 moduleID: 'neverEndingReddit',
12459 moduleName: 'Never Ending Reddit',
12462 // any configurable options you have go here...
12463 // options must have a type and a value..
12464 returnToPrevPage: {
12467 description: 'Return to the page you were last on when hitting "back" button?'
12472 description: 'Automatically load new page on scroll (if off, you click to load)'
12474 notifyWhenPaused: {
12477 description: 'Show a reminder to unpause Never-Ending Reddit after pausing'
12479 reversePauseIcon: {
12482 description: 'Show "paused" bars icon when auto-load is paused and "play" wedge icon when active'
12487 description: 'After auto-loading a certain number of pages, pause the auto-loader'
12488 + '<br><br>0 or a negative number means Never-Ending Reddit will only pause when you click'
12489 + ' the play/pause button in the top right corner.'
12495 { name: 'Fade', value: 'fade' },
12496 { name: 'Hide', value: 'hide' },
12497 { name: 'Do not hide', value: 'none' }
12499 description: 'Fade or completely hide duplicate posts from previous pages.'
12502 description: 'Inspired by modules like River of Reddit and Auto Pager - gives you a never ending stream of reddit goodness.',
12503 isEnabled: function() {
12504 return RESConsole.getModulePrefs(this.moduleID);
12507 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\_\?=]*/i
12511 isMatchURL: function() {
12512 return RESUtils.isMatchURL(this.moduleID);
12514 beforeLoad: function() {
12515 if ((this.isEnabled()) && (this.isMatchURL())) {
12516 RESUtils.addCSS('#NERModal { display: none; z-index: 999; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background-color: #333; opacity: 0.6; }');
12517 RESUtils.addCSS('#NERContent { display: none; position: fixed; top: 40px; z-index: 1000; width: 720px; background-color: #FFF; color: #000; padding: 10px; font-size: 12px; }');
12518 RESUtils.addCSS('#NERModalClose { position: absolute; top: 3px; right: 3px; }');
12519 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; }');
12520 RESUtils.addCSS('.NERdupe p.title:after { color: #000; font-size: 10px; content: \' (duplicate from previous page)\'; }');
12521 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; }');
12522 // hide next/prev page indicators
12523 RESUtils.addCSS('.content p.nextprev { display: none; } ');
12524 switch (this.options.hideDupes.value) {
12526 RESUtils.addCSS('.NERdupe { opacity: 0.3; }');
12529 RESUtils.addCSS('.NERdupe { display: none; }');
12532 // set the style for our little loader widget
12533 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; } ');
12534 RESUtils.addCSS('#progressIndicator h2 { margin-bottom: .5em; }');
12535 RESUtils.addCSS('#progressIndicator .gearIcon { margin-left: 1em; }');
12536 RESUtils.addCSS('#NREMailCount { margin-left: 0; float: left; margin-top: 3px;}');
12537 RESUtils.addCSS('#NREPause { margin-left: 2px; width: 16px; height: 16px; float: left; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); cursor: pointer; }');
12538 RESUtils.addCSS('#NREPause, #NREPause.paused.reversePause { background-position: -16px -192px; }');
12539 RESUtils.addCSS('#NREPause.paused, #NREPause.reversePause { background-position: 0 -192px; }');
12543 if ((this.isEnabled()) && (this.isMatchURL())) {
12545 /* if (RESUtils.pageType() !== 'linklist') {
12546 sessionStorage.NERpageURL = location.href;
12548 */ // modified from a contribution by Peter Siewert, thanks Peter!
12549 if (typeof modules['neverEndingReddit'].dupeHash === 'undefined') modules['neverEndingReddit'].dupeHash = {};
12550 var entries = document.body.querySelectorAll('a.comments');
12551 for (var i = entries.length - 1; i > -1; i--) {
12552 modules['neverEndingReddit'].dupeHash[entries[i].href] = 1;
12555 this.allLinks = document.body.querySelectorAll('#siteTable div.thing');
12557 // code inspired by River of Reddit, but rewritten from scratch to work across multiple browsers...
12558 // Original River of Reddit author: reddy kapil
12559 // Original link to Chrome extension: https://chrome.google.com/extensions/detail/bjiggjllfebckflfdjbimogjieeghcpp
12561 // store access to the siteTable div since that's where we'll append new data...
12562 var stMultiCheck = document.querySelectorAll('#siteTable');
12563 this.siteTable = stMultiCheck[0];
12564 // 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!)
12565 if (stMultiCheck.length === 2) {
12566 // console.log('skipped first sitetable, stupid reddit.');
12567 this.siteTable = stMultiCheck[1];
12569 // get the first link to the next page of reddit...
12570 var nextPrevLinks = document.body.querySelectorAll('.content .nextprev a');
12571 if (nextPrevLinks.length > 0) {
12572 var nextLink = nextPrevLinks[nextPrevLinks.length-1];
12574 this.nextPageURL = nextLink.getAttribute('href');
12575 var nextXY=RESUtils.getXYpos(nextLink);
12576 this.nextPageScrollY = nextXY.y;
12578 this.attachLoaderWidget();
12580 //Reset this info if the page is in a new tab
12581 // wait, this is always tre... commenting out.
12583 if (window.history.length) {
12584 console.log('delete nerpage');
12585 delete sessionStorage['NERpage'];
12587 if (this.options.returnToPrevPage.value) {
12588 // if the user clicks any external links, save that link
12589 // get all external links and track clicks...
12590 /* $('body').on('click', 'a.title[href^="http://"]', function(e) {
12591 // if left click and not going to open in a new tab...
12592 if ((this.target !== '_blank') && (e.which === 1)) sessionStorage.lastPageURL = this.href;
12595 this.returnToPrevPageCheck(location.hash);
12598 // watch for the user scrolling to the bottom of the page. If they do it, load a new page.
12599 if (this.options.autoLoad.value) {
12600 window.addEventListener('scroll', modules['neverEndingReddit'].handleScroll, false);
12603 // check if the user has new mail...
12604 this.navMail = document.body.querySelector('#mail');
12605 this.NREFloat = createElementWithID('div','NREFloat');
12606 this.NREPause = createElementWithID('div','NREPause');
12607 this.NREPause.setAttribute('title','Pause / Restart Never Ending Reddit');
12608 if (this.options.reversePauseIcon.value) this.NREPause.classList.add('reversePause');
12609 this.isPaused = (RESStorage.getItem('RESmodules.neverEndingReddit.isPaused') == true);
12610 if (this.isPaused) this.NREPause.classList.add('paused');
12611 this.NREPause.addEventListener('click',modules['neverEndingReddit'].togglePause, false);
12612 if ((modules['betteReddit'].options.pinHeader.value !== 'userbar') && (modules['betteReddit'].options.pinHeader.value !== 'header')) {
12613 this.NREMail = createElementWithID('a','NREMail');
12614 if (modules['betteReddit'].options.pinHeader.value === 'sub') {
12615 RESUtils.addCSS('#NREFloat { position: fixed; top: 23px; right: 8px; display: none; }');
12616 } else if (modules['betteReddit'].options.pinHeader.value === 'subanduser') {
12617 RESUtils.addCSS('#NREFloat { position: fixed; top: 44px; right: 0; display: none; }');
12618 RESUtils.addCSS('#NREMail { display: none; }');
12619 RESUtils.addCSS('#NREMailCount { display: none; }');
12621 RESUtils.addCSS('#NREFloat { position: fixed; top: 10px; right: 10px; display: none; }');
12623 RESUtils.addCSS('#NREMail { width: 16px; height: 12px; float: left; margin-top: 4px; background: center center no-repeat; }');
12624 RESUtils.addCSS('#NREMail.nohavemail { background-image: url(http://www.redditstatic.com/mailgray.png); }');
12625 RESUtils.addCSS('#NREMail.havemail { background-image: url(http://www.redditstatic.com/mail.png); }');
12626 RESUtils.addCSS('.res-colorblind #NREMail.havemail { background-image: url(http://thumbs.reddit.com/t5_2s10b_5.png); }');
12627 this.NREFloat.appendChild(this.NREMail);
12628 this.NREMailCount = createElementWithID('a','NREMailCount');
12629 this.NREMailCount.display = 'none';
12630 this.NREMailCount.setAttribute('href',modules['betteReddit'].getInboxLink(true));
12631 this.NREFloat.appendChild(this.NREMailCount);
12632 var hasNew = false;
12633 if ((typeof this.navMail !== 'undefined') && (this.navMail !== null)) {
12634 hasNew = this.navMail.classList.contains('havemail');
12636 this.setMailIcon(hasNew);
12638 this.NREMail = this.navMail;
12639 RESUtils.addCSS('#NREFloat { position: fixed; top: 30px; right: 8px; display: none; }');
12641 this.NREFloat.appendChild(this.NREPause);
12642 document.body.appendChild(this.NREFloat);
12647 togglePause: function() {
12648 modules['neverEndingReddit'].isPaused = !modules['neverEndingReddit'].isPaused;
12649 RESStorage.setItem('RESmodules.neverEndingReddit.isPaused', modules['neverEndingReddit'].isPaused);
12650 if (modules['neverEndingReddit'].isPaused) {
12651 modules['neverEndingReddit'].NREPause.classList.add('paused');
12652 if (modules['neverEndingReddit'].options.notifyWhenPaused.value) {
12653 var notification = [];
12654 notification.push('Never-Ending Reddit has been paused. Click the play/pause button to unpause it.');
12655 notification.push('To hide this message, disable Never-Ending Reddit\'s ' + modules['settingsNavigation'].makeUrlHashLink('neverEndingReddit', 'notifyWhenPaused', 'notifyWhenPaused option <span class="gearIcon" />') + '.');
12656 notification = notification.join('<br><br>');
12657 RESUtils.notification({
12658 moduleID: 'neverEndingReddit',
12659 message: notification
12663 modules['neverEndingReddit'].NREPause.classList.remove('paused');
12664 modules['neverEndingReddit'].handleScroll();
12666 modules['neverEndingReddit'].setWidgetActionText();
12668 returnToPrevPageCheck: function(hash) {
12669 var pageRE = /page=(\d+)/,
12670 match = pageRE.exec(hash);
12671 // Set the current page to page 1...
12674 var backButtonPageNumber = match[1] || 1;
12675 if (backButtonPageNumber > 1) {
12676 this.attachModalWidget();
12677 this.currPage = backButtonPageNumber;
12678 this.loadNewPage(true);
12683 if ((sessionStorage.NERpageURL) && (sessionStorage.NERpageURL != sessionStorage.lastPageURL)) {
12684 var backButtonPageNumber = sessionStorage.getItem('NERpage') || 1;
12685 if (backButtonPageNumber > 1) {
12686 this.currPage = backButtonPageNumber;
12687 this.loadNewPage(true);
12690 sessionStorage.lastPageURL = location.href;
12693 handleScroll: function(e) {
12694 if (modules['neverEndingReddit'].scrollTimer) clearTimeout(modules['neverEndingReddit'].scrollTimer);
12695 modules['neverEndingReddit'].scrollTimer = setTimeout(modules['neverEndingReddit'].handleScrollAfterTimer, 300);
12697 handleScrollAfterTimer: function(e) {
12698 var thisPageNum = 1,
12701 for (var i=0, len=modules['neverEndingReddit'].pageMarkers.length; i<len; i++) {
12702 var thisXY = RESUtils.getXYpos(modules['neverEndingReddit'].pageMarkers[i]);
12703 if (thisXY.y < window.pageYOffset) {
12704 thisMarker = modules['neverEndingReddit'].pageMarkers[i];
12705 thisPageNum = thisMarker.getAttribute('id').replace('page-','');
12706 modules['neverEndingReddit'].currPage = thisPageNum;
12708 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
12709 RESStorage.setItem('RESmodules.neverEndingReddit.lastPage.'+thisPageType, thisMarker.getAttribute('url'));
12715 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
12716 // RESStorage.setItem('RESmodules.neverEndingReddit.lastPage.'+thisPageType, modules['neverEndingReddit'].pageURLs[thisPageNum]);
12717 if (thisPageNum != sessionStorage.NERpage) {
12718 if (thisPageNum > 1) {
12719 // sessionStorage.NERpageURL = location.href;
12720 sessionStorage.NERpage = thisPageNum;
12721 modules['neverEndingReddit'].pastFirstPage = true;
12722 location.hash = 'page='+thisPageNum;
12724 if (location.hash.indexOf('page=') !== -1) {
12725 location.hash = 'page='+thisPageNum;
12727 delete sessionStorage['NERpage'];
12730 if ((modules['neverEndingReddit'].fromBackButton != true) && (modules['neverEndingReddit'].options.returnToPrevPage.value)) {
12731 for (var i=0, len=modules['neverEndingReddit'].allLinks.length; i<len; i++) {
12732 if (RESUtils.elementInViewport(modules['neverEndingReddit'].allLinks[i])) {
12733 var thisClassString = modules['neverEndingReddit'].allLinks[i].getAttribute('class');
12734 var thisClass = thisClassString.match(/id-t[\d]_[\w]+/);
12736 var thisID = thisClass[0];
12737 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
12738 RESStorage.setItem('RESmodules.neverEndingReddit.lastVisibleIndex.'+thisPageType, thisID);
12744 if ((RESUtils.elementInViewport(modules['neverEndingReddit'].progressIndicator)) && (modules['neverEndingReddit'].fromBackButton != true)) {
12745 if (modules['neverEndingReddit'].isPaused != true) {
12746 modules['neverEndingReddit'].loadNewPage();
12747 modules['neverEndingReddit'].pauseAfter(thisPageNum);
12750 if ($(window).scrollTop() > 30) {
12751 modules['neverEndingReddit'].showFloat(true);
12753 modules['neverEndingReddit'].showFloat(false);
12756 pauseAfterPages: null,
12757 pauseAfter: function(currPageNum) {
12758 if (this.pauseAfterPages === null) {
12759 this.pauseAfterPages = parseInt(modules['neverEndingReddit'].options.pauseAfterEvery.value);
12762 if ((this.pauseAfterPages > 0) && (currPageNum % this.pauseAfterPages === 0)) {
12763 this.togglePause(true);
12764 var notification = [];
12765 notification.push('Time for a break!');
12766 notification.push('Never-Ending Reddit was paused automatically. ' + modules['settingsNavigation'].makeUrlHashLink('neverEndingReddit', 'pauseAfterEvery', '', 'gearIcon'));
12767 notification = notification.join('<br><br>');
12768 setTimeout(RESUtils.notification.bind(RESUtils, notification, 5000));
12771 duplicateCheck: function(newHTML){
12772 var newLinks = newHTML.querySelectorAll('div.link');
12773 for(var i = newLinks.length - 1; i > -1; i--) {
12774 var newLink = newLinks[i];
12775 var thisCommentLink = newLink.querySelector('a.comments').href;
12776 if( modules['neverEndingReddit'].dupeHash[thisCommentLink] ) {
12777 // let's not remove it altogether, but instead dim it...
12778 // newLink.parentElement.removeChild(newLink);
12779 newLink.classList.add('NERdupe');
12781 modules['neverEndingReddit'].dupeHash[thisCommentLink] = 1;
12786 setMailIcon: function(newmail) {
12787 if (RESUtils.loggedInUser() === null) return false;
12789 modules['neverEndingReddit'].hasNewMail = true;
12790 this.NREMail.classList.remove('nohavemail');
12791 this.NREMail.setAttribute('href', modules['betteReddit'].getInboxLink(true));
12792 this.NREMail.setAttribute('title','new mail!');
12793 this.NREMail.classList.add('havemail');
12794 modules['betteReddit'].showUnreadCount();
12796 modules['neverEndingReddit'].hasNewMail = false;
12797 this.NREMail.classList.add('nohavemail');
12798 this.NREMail.setAttribute('href',modules['betteReddit'].getInboxLink(false));
12799 this.NREMail.setAttribute('title','no new mail');
12800 this.NREMail.classList.remove('havemail');
12801 modules['betteReddit'].setUnreadCount(0);
12804 attachModalWidget: function() {
12805 this.modalWidget = createElementWithID('div','NERModal');
12806 $(this.modalWidget).html(' ');
12807 this.modalContent = createElementWithID('div','NERContent');
12808 $(this.modalContent).html('<div id="NERModalClose" class="RESCloseButton">×</div>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.<br><img src="'+RESConsole.loader+'">');
12809 document.body.appendChild(this.modalWidget);
12810 document.body.appendChild(this.modalContent);
12811 $('#NERModalClose').click(function() {
12812 $(modules['neverEndingReddit'].modalWidget).hide();
12813 $(modules['neverEndingReddit'].modalContent).hide();
12816 attachLoaderWidget: function() {
12817 // 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...
12818 this.progressIndicator = document.createElement('div');
12819 this.setWidgetActionText();
12820 this.progressIndicator.id = 'progressIndicator';
12821 this.progressIndicator.className = 'neverEndingReddit';
12823 this.progressIndicator.addEventListener('click', function(e) {
12824 if (e.target.id !== 'NERStaticLink' && !e.target.classList.contains('gearIcon')) {
12825 e.preventDefault();
12826 modules['neverEndingReddit'].loadNewPage();
12829 insertAfter(this.siteTable, this.progressIndicator);
12831 setWidgetActionText: function () {
12832 $(this.progressIndicator).empty();
12833 $('<h2>Never Ending Reddit</h2>')
12834 .appendTo(this.progressIndicator)
12835 .append(modules['settingsNavigation'].makeUrlHashLink('neverEndingReddit', null, ' ', 'gearIcon'));
12837 var text = "Click to load the next page";
12838 if (this.options.autoLoad.value && !this.isPaused) {
12839 text = "scroll or click to load the next page";
12840 } else if (this.options.autoLoad.value && this.isPaused) {
12841 text = "click to load the next page; or click the 'pause' button in the top right corner"
12846 .appendTo(this.progressIndicator);
12848 var nextpage = $('<a id="NERStaticLink">or open next page</a>')
12849 .attr('href', this.nextPageURL);
12850 $('<p />').append(nextpage)
12851 .append(' (and clear Never-Ending stream)')
12852 .appendTo(this.progressIndicator);
12854 loadNewPage: function(fromBackButton, reload) {
12855 var me = modules['neverEndingReddit'];
12856 if (me.isLoading != true) {
12857 me.isLoading = true;
12858 if (fromBackButton) {
12859 me.fromBackButton = true;
12860 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
12861 var savePageURL = me.nextPageURL;
12862 me.nextPageURL = RESStorage.getItem('RESmodules.neverEndingReddit.lastPage.'+thisPageType);
12863 if ((me.nextPageURL === 'undefined') || (me.nextPageURL == null)) {
12864 // something went wrong, probably someone hit refresh. Just revert to the first page...
12865 modules['neverEndingReddit'].fromBackButton = false;
12866 me.nextPageURL = savePageURL;
12868 me.isLoading = false;
12871 var leftCentered = Math.floor((window.innerWidth - 720) / 2);
12872 me.modalWidget.style.display = 'block';
12873 me.modalContent.style.display = 'block';
12874 me.modalContent.style.left = leftCentered + 'px';
12875 // remove the progress indicator early, as we don't want the user to scroll past it on accident, loading more content.
12876 me.progressIndicator.parentNode.removeChild(modules['neverEndingReddit'].progressIndicator);
12878 me.fromBackButton = false;
12882 me.progressIndicator.removeEventListener('click', modules['neverEndingReddit'].loadNewPage , false);
12883 $(me.progressIndicator).html('<img src="'+RESConsole.loader+'"> Loading next page...');
12884 // as a sanity check, which should NEVER register true, we'll make sure me.nextPageURL is on the same domain we're browsing...
12885 if (me.nextPageURL.indexOf(location.hostname) === -1) {
12886 console.log('Next page URL mismatch. Something strange may be afoot.')
12887 me.isLoading = false;
12890 GM_xmlhttpRequest({
12892 url: me.nextPageURL,
12893 onload: function(response) {
12894 if ((typeof modules['neverEndingReddit'].progressIndicator.parentNode !== 'undefined') && (modules['neverEndingReddit'].progressIndicator.parentNode !== null)) {
12895 modules['neverEndingReddit'].progressIndicator.parentNode.removeChild(modules['neverEndingReddit'].progressIndicator);
12897 // drop the HTML we got back into a div...
12898 var thisHTML = response.responseText;
12899 var tempDiv = document.createElement('div');
12900 // clear out any javascript so we don't render it again...
12901 $(tempDiv).html(thisHTML.replace(/<script(.|\s)*?\/script>/g, ''));
12902 // grab the siteTable out of there...
12903 var newHTML = tempDiv.querySelector('#siteTable');
12904 // did we find anything?
12905 if (newHTML !== null) {
12906 var stMultiCheck = tempDiv.querySelectorAll('#siteTable');
12907 // 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!)
12908 if (stMultiCheck.length === 2) {
12909 // console.log('skipped first sitetable, stupid reddit.');
12910 newHTML = stMultiCheck[1];
12912 newHTML.setAttribute('ID','siteTable-'+modules['neverEndingReddit'].currPage+1);
12913 modules['neverEndingReddit'].duplicateCheck(newHTML);
12914 // check for new mail
12915 var hasNewMail = tempDiv.querySelector('#mail');
12916 if ((typeof hasNewMail !== 'undefined') && (hasNewMail !== null) && (hasNewMail.classList.contains('havemail'))) {
12917 modules['neverEndingReddit'].setMailIcon(true);
12919 modules['neverEndingReddit'].setMailIcon(false);
12921 // load up uppers and downers, if enabled...
12922 // maybe not necessary anymore..
12924 if ((modules['uppersAndDowners'].isEnabled()) && (RESUtils.pageType() === 'comments')) {
12925 modules['uppersAndDowners'].applyUppersAndDownersToComments(modules['neverEndingReddit'].nextPageURL);
12928 // get the new nextLink value for the next page...
12929 var nextPrevLinks = tempDiv.querySelectorAll('.content .nextprev a');
12930 if ((nextPrevLinks) && (nextPrevLinks.length)) {
12931 if (isNaN(modules['neverEndingReddit'].currPage)) modules['neverEndingReddit'].currPage = 1;
12932 if (!fromBackButton) modules['neverEndingReddit'].currPage++;
12933 if ((!(modules['neverEndingReddit'].fromBackButton)) && (modules['neverEndingReddit'].options.returnToPrevPage.value)) {
12934 // modules['neverEndingReddit'].pageURLs[modules['neverEndingReddit'].currPage] = modules['neverEndingReddit'].nextPageURL;
12935 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
12936 RESStorage.setItem('RESmodules.neverEndingReddit.lastPage.'+thisPageType, modules['neverEndingReddit'].nextPageURL);
12937 // location.hash = 'page='+modules['neverEndingReddit'].currPage;
12939 var nextLink = nextPrevLinks[nextPrevLinks.length-1];
12940 var nextPage = modules['neverEndingReddit'].currPage;
12941 var pageMarker = createElementWithID('div','page-'+nextPage);
12942 pageMarker.classList.add('NERPageMarker');
12943 $(pageMarker).text('Page ' + nextPage);
12944 modules['neverEndingReddit'].siteTable.appendChild(pageMarker);
12945 modules['neverEndingReddit'].pageMarkers.push(pageMarker);
12946 modules['neverEndingReddit'].siteTable.appendChild(newHTML);
12947 modules['neverEndingReddit'].isLoading = false;
12949 // console.log(nextLink);
12950 pageMarker.setAttribute('url',me.nextPageURL);
12951 if (nextLink.getAttribute('rel').indexOf('prev') !== -1) {
12952 // remove the progress indicator from the DOM, it needs to go away.
12953 modules['neverEndingReddit'].progressIndicator.style.display = 'none';
12954 var endOfReddit = createElementWithID('div','endOfReddit');
12955 $(endOfReddit).text('You\'ve reached the last page available. There are no more pages to load.');
12956 modules['neverEndingReddit'].siteTable.appendChild(endOfReddit);
12957 window.removeEventListener('scroll', modules['neverEndingReddit'].handleScroll, false);
12959 // console.log('not over yet');
12960 var prevLink = nextPrevLinks[0];
12961 modules['neverEndingReddit'].nextPageURL = nextLink.getAttribute('href');
12962 modules['neverEndingReddit'].attachLoaderWidget();
12965 modules['neverEndingReddit'].allLinks = document.body.querySelectorAll('#siteTable div.thing');
12966 if ((fromBackButton) && (modules['neverEndingReddit'].options.returnToPrevPage.value)) {
12967 // TODO: it'd be great to figure out a better way than a timeout, but this
12968 // has considerably helped the accuracy of RES's ability to return you to where
12970 setTimeout(modules['neverEndingReddit'].scrollToLastElement, 4000);
12973 // If we're on the reddit-browsing page (/reddits or /subreddits), add +shortcut and -shortcut buttons...
12974 if (/^https?:\/\/www\.reddit\.com\/(?:sub)?reddits\/?(?:\?[\w=&]+)*/.test(location.href)) {
12975 modules['subredditManager'].browsingReddits();
12978 var noresults = tempDiv.querySelector('#noresults');
12979 var noresultsfound = (noresults !== null);
12980 modules['neverEndingReddit'].NERFail(noresultsfound);
12982 var e = document.createEvent("Events");
12983 e.initEvent("neverEndingLoad", true, true);
12984 window.dispatchEvent(e);
12987 onerror: function(err) {
12988 modules['neverEndingReddit'].NERFail();
12992 // console.log('load new page ignored');
12995 scrollToLastElement: function() {
12996 modules['neverEndingReddit'].modalWidget.style.display = 'none';
12997 modules['neverEndingReddit'].modalContent.style.display = 'none';
12998 // window.scrollTo(0,0)
12999 // RESUtils.scrollTo(0,modules['neverEndingReddit'].nextPageScrollY);
13000 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
13001 var lastTopScrolledID = RESStorage.getItem('RESmodules.neverEndingReddit.lastVisibleIndex.'+thisPageType);
13002 var lastTopScrolledEle = document.body.querySelector('.'+lastTopScrolledID);
13003 if (!lastTopScrolledEle) {
13004 var lastTopScrolledEle = newHTML.querySelector('#siteTable div.thing');
13006 var thisXY=RESUtils.getXYpos(lastTopScrolledEle);
13007 RESUtils.scrollTo(0, thisXY.y);
13008 modules['neverEndingReddit'].fromBackButton = false;
13010 NERFail: function(noresults) {
13011 modules['neverEndingReddit'].isLoading = false;
13012 var newHTML = createElementWithID('div','NERFail');
13014 $(newHTML).html('Reddit has responded "there doesn\'t seem to be anything here." - this sometimes happens after several pages as votes shuffle posts up and down. You\'ll have to <a href="'+location.href.split('#')[0]+'">start from the beginning.</a> If you like, you can try loading <a target="_blank" href="'+modules['neverEndingReddit'].nextPageURL+'">the same URL that RES tried to load.</a> If you are interested, there is a <a target="_blank" href="http://www.reddit.com/r/Enhancement/comments/s72xt/never_ending_reddit_and_reddit_barfing_explained/">technical explanation here</a>.');
13015 newHTML.setAttribute('style','cursor: auto !important;');
13017 $(newHTML).text('It appears Reddit is under heavy load or has barfed for some other reason, so Never Ending Reddit couldn\'t load the next page. Click here to try to load the page again.');
13018 newHTML.addEventListener('click', function(e) {
13019 modules['neverEndingReddit'].attachLoaderWidget();
13020 modules['neverEndingReddit'].loadNewPage(false, true);
13021 e.target.parentNode.removeChild(e.target);
13022 e.target.textContent = 'Loading... or trying, anyway...';
13025 modules['neverEndingReddit'].siteTable.appendChild(newHTML);
13026 modules['neverEndingReddit'].modalWidget.style.display = 'none';
13027 modules['neverEndingReddit'].modalContent.style.display = 'none';
13029 showFloat: function(show) {
13031 this.NREFloat.style.display = 'block';
13033 this.NREFloat.style.display = 'none';
13038 modules['saveComments'] = {
13039 moduleID: 'saveComments',
13040 moduleName: 'Save Comments',
13041 category: 'Comments',
13043 // any configurable options you have go here...
13044 // options must have a type and a value..
13045 // valid types are: text, boolean (if boolean, value must be true or false)
13048 description: 'Save Comments saves the full text of comments locally in your browser, unlike Reddit\'s "save" feature, which saves a link to the comment.',
13049 isEnabled: function() {
13050 return RESConsole.getModulePrefs(this.moduleID);
13053 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
13056 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*\/submit\/?/i,
13057 /^https?:\/\/([a-z]+)\.reddit\.com\/submit\/?/i
13059 isMatchURL: function() {
13060 return RESUtils.isMatchURL(this.moduleID);
13062 beforeLoad: function() {
13063 if ((this.isEnabled()) && (this.isMatchURL())) {
13064 RESUtils.addCSS('.RES-save { cursor: help; }');
13068 if ((this.isEnabled()) && (this.isMatchURL())) {
13069 var currURL = location.href;
13070 var commentsRegex = /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*comments\/[-\w\.\/]*/i;
13071 var savedRegex = /^https?:\/\/([a-z]+)\.reddit\.com\/user\/([-\w]+)\/saved\/?/i;
13072 if (commentsRegex.test(currURL)) {
13073 // load already-saved comments into memory...
13074 this.loadSavedComments();
13075 this.addSaveLinks();
13076 $('body').on('click', 'li.saveComments', function(e) {
13077 e.preventDefault();
13078 modules['saveComments'].saveComment(this, this.getAttribute('saveID'), this.getAttribute('saveLink'), this.getAttribute('saveUser'));
13080 $('body').on('click', 'li.unsaveComments', function(e) {
13081 // e.preventDefault();
13082 var id = this.getAttribute('unsaveID');
13083 modules['saveComments'].unsaveComment(id, this);
13085 } else if (savedRegex.test(currURL)) {
13086 // load already-saved comments into memory...
13087 this.loadSavedComments();
13088 this.addSavedCommentsTab();
13089 this.drawSavedComments();
13090 if (location.hash === '#comments') {
13091 this.showSavedTab('comments');
13094 this.addSavedCommentsTab();
13096 // Watch for any future 'reply' forms, or stuff loaded in via "load more comments"...
13098 document.body.addEventListener(
13100 function( event ) {
13101 if ((event.target.tagName === 'DIV') && (hasClass(event.target,'thing'))) {
13102 modules['saveComments'].addSaveLinks(event.target);
13108 RESUtils.watchForElement('newComments', modules['saveComments'].addSaveLinks);
13111 addSaveLinks: function(ele) {
13112 if (!ele) var ele = document.body;
13113 var allComments = ele.querySelectorAll('div.commentarea > div.sitetable > div.thing div.entry div.noncollapsed');
13114 RESUtils.forEachChunked(allComments, 15, 1000, function(comment, i, array) {
13115 modules['saveComments'].addSaveLinkToComment(comment);
13118 addSaveLinkToComment: function(commentObj) {
13119 var commentsUL = commentObj.querySelector('ul.flat-list');
13120 var permaLink = commentsUL.querySelector('li.first a.bylink');
13121 if (permaLink !== null) {
13122 // if there's no 'parent' link, then we don't want to put the save link before 'lastchild', we need to move it one to the left..
13123 // note that if the user is not logged in, there is no next link for first level comments... set to null!
13124 if (RESUtils.loggedInUser()) {
13125 if (permaLink.parentNode.nextSibling !== null) {
13126 if (typeof permaLink.parentNode.nextSibling.firstChild.getAttribute !== 'undefined') {
13127 var nextLink = permaLink.parentNode.nextSibling.firstChild.getAttribute('href');
13129 var nextLink = null;
13132 var nextLink = null;
13135 var nextLink = null;
13137 var isTopLevel = ((nextLink == null) || (nextLink.indexOf('#') === -1));
13138 var userLink = commentObj.querySelector('a.author');
13139 if (userLink == null) {
13140 var saveUser = '[deleted]';
13142 var saveUser = userLink.text;
13144 var saveHREF = permaLink.getAttribute('href');
13145 var splitHref = saveHREF.split('/');
13146 var saveID = splitHref[splitHref.length-1];
13147 var saveLink = document.createElement('li');
13148 if ((typeof this.storedComments !== 'undefined') && (typeof this.storedComments[saveID] !== 'undefined')) {
13149 $(saveLink).html('<a class="RES-saved" href="/saved#comments">saved-RES</a>');
13151 $(saveLink).html('<a class="RES-save" href="javascript:void(0);" title="Save using RES - which is local only, but preserves the full text in case someone edits/deletes it">save-RES</a>');
13152 saveLink.setAttribute('class', 'saveComments');
13153 saveLink.setAttribute('saveID',saveID);
13154 saveLink.setAttribute('saveLink',saveHREF);
13155 saveLink.setAttribute('saveUser',saveUser);
13157 var whereToInsert = commentsUL.lastChild;
13158 if (isTopLevel) whereToInsert = whereToInsert.previousSibling;
13159 commentsUL.insertBefore(saveLink, whereToInsert);
13162 loadSavedComments: function() {
13163 // first, check if we're storing saved comments the old way (as an array)...
13164 var thisComments = RESStorage.getItem('RESmodules.saveComments.savedComments');
13165 if (thisComments == null) {
13166 this.storedComments = {};
13168 this.storedComments = safeJSON.parse(thisComments, 'RESmodules.saveComments.savedComments');
13169 // old way of storing saved comments... convert...
13170 if (thisComments.slice(0,1) === '[') {
13171 var newFormat = {};
13172 for (var i in this.storedComments) {
13173 var urlSplit = this.storedComments[i].href.split('/');
13174 var thisID = urlSplit[urlSplit.length-1];
13175 newFormat[thisID] = this.storedComments[i];
13177 this.storedComments = newFormat;
13178 RESStorage.setItem('RESmodules.saveComments.savedComments',JSON.stringify(newFormat));
13182 saveComment: function(obj, id, href, username, comment) {
13183 // reload saved comments in case they've been updated in other tabs (works in all but greasemonkey)
13184 this.loadSavedComments();
13185 // loop through comments and make sure we haven't already saved this one...
13186 if (typeof this.storedComments[id] !== 'undefined') {
13187 alert('comment already saved!');
13189 if (modules['keyboardNav'].isEnabled()) {
13190 // unfocus it before we save it so we don't save the keyboard annotations...
13191 modules['keyboardNav'].keyUnfocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
13193 var comment = obj.parentNode.parentNode.querySelector('div.usertext-body > div.md');
13194 if (comment !== null) {
13195 commentHTML = comment.innerHTML;
13196 var savedComment = {
13198 username: username,
13199 comment: commentHTML,
13202 this.storedComments[id] = savedComment;
13203 var unsaveObj = document.createElement('li');
13204 $(unsaveObj).html('<a href="javascript:void(0);">unsave-RES</a>');
13205 unsaveObj.setAttribute('unsaveID',id);
13206 unsaveObj.setAttribute('unsaveLink',href);
13207 unsaveObj.setAttribute('class','unsaveComments');
13209 obj.parentNode.replaceChild(unsaveObj, obj);
13211 if (modules['keyboardNav'].isEnabled()) {
13212 modules['keyboardNav'].keyFocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
13214 if (RESUtils.proEnabled()) {
13215 // add sync adds/deletes for RES Pro.
13216 if (typeof this.storedComments.RESPro_add === 'undefined') {
13217 this.storedComments.RESPro_add = {}
13219 if (typeof this.storedComments.RESPro_delete === 'undefined') {
13220 this.storedComments.RESPro_delete = {}
13222 // add this ID next time we sync...
13223 this.storedComments.RESPro_add[id] = true;
13224 // make sure we don't run a delete on this ID next time we sync...
13225 if (typeof this.storedComments.RESPro_delete[id] !== 'undefined') delete this.storedComments.RESPro_delete[id];
13227 RESStorage.setItem('RESmodules.saveComments.savedComments', JSON.stringify(this.storedComments));
13228 if (RESUtils.proEnabled()) {
13229 modules['RESPro'].authenticate(function() {
13230 modules['RESPro'].saveModuleData('saveComments');
13235 addSavedCommentsTab: function() {
13236 var mainmenuUL = document.body.querySelector('#header-bottom-left ul.tabmenu');
13238 var savedRegex = /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w]+\/saved\/?/i;
13239 var menuItems = mainmenuUL.querySelectorAll('li');
13240 var thisUser = RESUtils.loggedInUser() || '';
13241 for (var i=0, len=menuItems.length;i<len;i++) {
13242 var savedLink = menuItems[i].querySelector('a');
13243 if ((menuItems[i].classList.contains('selected')) && (savedRegex.test(savedLink.href))) {
13244 menuItems[i].addEventListener('click', function(e) {
13245 e.preventDefault();
13246 modules['saveComments'].showSavedTab('links');
13250 if (savedRegex.test(savedLink.href)) {
13251 $(menuItems[i]).attr('id', 'savedLinksTab');
13252 savedLink.textContent = 'saved links';
13256 var savedCommentsTab = $('<li id="savedCommentsTab">')
13257 .html('<a href="javascript:void(0);">saved comments</a>')
13258 .insertAfter('#savedLinksTab');
13259 if (savedRegex.test(location.href)) {
13260 $('#savedCommentsTab').click(function(e) {
13261 e.preventDefault();
13262 modules['saveComments'].showSavedTab('comments');
13265 $('#savedCommentsTab').click(function(e) {
13266 e.preventDefault();
13267 location.href = location.protocol + '//www.reddit.com/saved/#comments';
13272 showSavedTab: function(tab) {
13275 location.hash = 'links';
13276 this.savedLinksContent.style.display = 'block';
13277 this.savedCommentsContent.style.display = 'none';
13278 $('#savedLinksTab').addClass('selected');
13279 $('#savedCommentsTab').removeClass('selected');
13282 location.hash = 'comments';
13283 this.savedLinksContent.style.display = 'none';
13284 this.savedCommentsContent.style.display = 'block';
13285 $('#savedLinksTab').removeClass('selected');
13286 $('#savedCommentsTab').addClass('selected');
13290 drawSavedComments: function() {
13291 RESUtils.addCSS('.savedComment { padding: 5px; font-size: 12px; margin-bottom: 20px; margin-left: 40px; margin-right: 10px; border: 1px solid #CCC; border-radius: 10px; width: auto; } ');
13292 RESUtils.addCSS('.savedCommentHeader { margin-bottom: 8px; }');
13293 RESUtils.addCSS('.savedCommentBody { margin-bottom: 8px; }');
13294 RESUtils.addCSS('#savedLinksList { margin-top: 10px; }');
13295 // css += '.savedCommentFooter { }';
13296 this.savedLinksContent = document.body.querySelector('BODY > div.content');
13297 this.savedCommentsContent = createElementWithID('div', 'savedLinksList');
13298 this.savedCommentsContent.style.display = 'none';
13299 this.savedCommentsContent.setAttribute('class','sitetable linklisting');
13300 for (var i in this.storedComments) {
13301 if ((i !== 'RESPro_add') && (i !== 'RESPro_delete')) {
13302 var clearLeft = document.createElement('div');
13303 clearLeft.setAttribute('class','clearleft');
13304 var thisComment = document.createElement('div');
13305 thisComment.classList.add('savedComment');
13306 thisComment.classList.add('entry');
13307 // this is all saved locally, but just for safety, we'll clean out any script tags and whatnot...
13308 var cleanHTML = '<div class="savedCommentHeader">Comment by user: ' + escapeHTML(this.storedComments[i].username) + ' saved on ' + escapeHTML(this.storedComments[i].timeSaved) + '</div>';
13309 cleanHTML += '<div class="savedCommentBody">' + this.storedComments[i].comment.replace(/<script(.|\s)*?\/script>/g, '') + '</div>';
13310 cleanHTML += '<div class="savedCommentFooter"><ul class="flat-list buttons"><li><a class="unsaveComment" href="javascript:void(0);">unsave-RES</a></li><li><a href="' + escapeHTML(this.storedComments[i].href) + '">view original</a></li></ul></div>'
13311 $(thisComment).html(cleanHTML);
13312 var unsaveLink = thisComment.querySelector('.unsaveComment');
13313 unsaveLink.setAttribute('unsaveID', i);
13314 unsaveLink.setAttribute('unsaveLink', this.storedComments[i].href);
13315 unsaveLink.addEventListener('click', function(e) {
13316 e.preventDefault();
13317 modules['saveComments'].unsaveComment(this.getAttribute('unsaveID'));
13319 this.savedCommentsContent.appendChild(thisComment);
13320 this.savedCommentsContent.appendChild(clearLeft);
13323 if (this.storedComments.length === 0) {
13324 $(this.savedCommentsContent).html('<li>You have not yet saved any comments.</li>');
13326 insertAfter(this.savedLinksContent, this.savedCommentsContent);
13328 unsaveComment: function(id, unsaveLink) {
13330 var newStoredComments = [];
13331 for (var i=0, len=this.storedComments.length;i<len;i++) {
13332 if (this.storedComments[i].href != href) {
13333 newStoredComments.push(this.storedComments[i]);
13335 // console.log('found match. deleted comment');
13338 this.storedComments = newStoredComments;
13340 delete this.storedComments[id];
13341 if (RESUtils.proEnabled()) {
13342 // add sync adds/deletes for RES Pro.
13343 if (typeof this.storedComments.RESPro_add === 'undefined') {
13344 this.storedComments.RESPro_add = {}
13346 if (typeof this.storedComments.RESPro_delete === 'undefined') {
13347 this.storedComments.RESPro_delete = {}
13349 // delete this ID next time we sync...
13350 this.storedComments.RESPro_delete[id] = true;
13351 // make sure we don't run an add on this ID next time we sync...
13352 if (typeof this.storedComments.RESPro_add[id] !== 'undefined') delete this.storedComments.RESPro_add[id];
13354 RESStorage.setItem('RESmodules.saveComments.savedComments', JSON.stringify(this.storedComments));
13355 if (RESUtils.proEnabled()) {
13356 modules['RESPro'].authenticate(function() {
13357 modules['RESPro'].saveModuleData('saveComments');
13360 if (typeof this.savedCommentsContent !== 'undefined') {
13361 this.savedCommentsContent.parentNode.removeChild(this.savedCommentsContent);
13362 this.drawSavedComments();
13363 this.showSavedTab('comments');
13365 var commentObj = unsaveLink.parentNode.parentNode;
13366 unsaveLink.parentNode.removeChild(unsaveLink);
13367 this.addSaveLinkToComment(commentObj);
13372 modules['userHighlight'] = {
13373 moduleID: 'userHighlight',
13374 moduleName: 'User Highlighter',
13376 description: 'Highlights certain users in comment threads: OP, Admin, Friends, Mod - contributed by MrDerk',
13381 description: 'Highlight OP\'s comments'
13386 description: 'Color to use to highlight OP. Defaults to original text color'
13391 description: 'Color used to highlight OP on hover.'
13396 description: 'Highlight Admin\'s comments'
13401 description: 'Color to use to highlight Admins. Defaults to original text color'
13406 description: 'Color used to highlight Admins on hover.'
13411 description: 'Highlight Friends\' comments'
13416 description: 'Color to use to highlight Friends. Defaults to original text color'
13418 friendColorHover: {
13421 description: 'Color used to highlight Friends on hover.'
13426 description: 'Highlight Mod\'s comments'
13431 description: 'Color to use to highlight Mods. Defaults to original text color'
13436 description: 'Color used to highlight Mods on hover. Defaults to gray.'
13438 highlightFirstCommenter: {
13441 description: 'Highlight the person who has the first comment in a tree, within that tree'
13443 firstCommentColor: {
13446 description: 'Color to use to highlight the first-commenter. Defaults to original text color'
13448 firstCommentColorHover: {
13451 description: 'Color used to highlight the first-commenter on hover.'
13456 description: 'Color for highlighted text.'
13458 autoColorUsernames: {
13461 description: 'Set a unique color for each username'
13464 isEnabled: function() {
13465 return RESConsole.getModulePrefs(this.moduleID);
13468 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
13470 isMatchURL: function() {
13471 return RESUtils.isMatchURL(this.moduleID);
13474 if ((this.isEnabled()) && (this.isMatchURL())) {
13475 this.findDefaults();
13476 if (this.options.highlightOP.value) this.doHighlight('submitter');
13477 if (this.options.highlightFriend.value) this.doHighlight('friend');
13478 if (this.options.highlightMod.value) this.doHighlight('moderator');
13479 if (this.options.highlightAdmin.value) this.doHighlight('admin');
13481 if (this.options.autoColorUsernames.value) {
13482 RESUtils.watchForElement('newComments', this.scanPageForNewUsernames);
13483 RESUtils.watchForElement('siteTable', this.scanPageForNewUsernames);
13484 this.scanPageForNewUsernames();
13487 if (this.options.highlightFirstCommenter.value) {
13488 RESUtils.watchForElement('newComments', this.scanPageForFirstComments);
13489 this.scanPageForFirstComments();
13493 findDefaults: function() {
13494 var dummy = $('<div style="height: 0px;" id="dummy" class="tagline">\
13495 <a class="author submitter">submitter</a>\
13496 <a class="author friend">friend</a>\
13497 <a class="author moderator">moderator</a>\
13498 <a class="author admin">admin</a>\
13500 $(document.body).append(dummy);
13501 this.colorTable = {
13503 default: RESUtils.getComputedStyle('#dummy .author.submitter', 'color'),
13504 color: this.options.OPColor.value,
13505 hoverColor: this.options.OPColorHover.value
13508 default: RESUtils.getComputedStyle('#dummy .author.friend', 'color'),
13509 color: this.options.friendColor.value,
13510 hoverColor: this.options.friendColorHover.value
13513 default: RESUtils.getComputedStyle('#dummy .author.moderator', 'color'),
13514 color: this.options.modColor.value,
13515 hoverColor: this.options.modColorHover.value
13518 default: RESUtils.getComputedStyle('#dummy .author.admin', 'color'),
13519 color: this.options.adminColor.value,
13520 hoverColor: this.options.adminColorHover.value
13523 default: '#5544CC',
13524 color: modules['userTagger'].options['highlightColor'].value,
13525 hoverColor: modules['userTagger'].options['highlightColorHover'].value,
13528 default: '#46B6CC',
13529 color: this.options.firstCommentColor.value,
13530 hoverColor: this.options.firstCommentColorHover.value
13533 $('#dummy').detach();
13535 scanPageForFirstComments: function (ele) {
13537 ? $(ele).closest('.commentarea > .sitetable > .thing')
13538 : document.body.querySelectorAll('.commentarea > .sitetable > .thing');
13541 RESUtils.forEachChunked(comments, 15, 1000, function(element, i, array) {
13544 for (var i = 0, length = element.classList.length; i < length; i++) {
13545 idClass = element.classList[i];
13546 if (idClass.substring(0, 6) === 'id-t1_') break;
13549 if (modules['userHighlight'].firstComments[idClass]) return;
13551 var author = element.querySelector('.author');
13552 if (!author) return;
13554 for (var i = 0, length = author.classList.length; i < length; i++) {
13555 authorClass = author.classList[i];
13556 if (authorClass.substring(0, 6) === 'id-t2_') break;
13559 var authorDidReply = element.querySelector('.child .' + authorClass);
13560 if (!authorDidReply) return;
13562 modules['userHighlight'].firstComments[idClass] = true;
13563 modules['userHighlight'].doHighlight('firstComment', authorClass, '.' + idClass);
13567 scanPageForNewUsernames: function (ele) {
13568 ele = ele || document.body;
13569 var authors = ele.querySelectorAll('.author');
13570 RESUtils.forEachChunked(authors, 15, 1000, function(element, i, array) {
13573 for (var i = 0, length = element.classList.length; i < length; i++) {
13574 idClass = element.classList[i];
13575 if (idClass.substring(0, 6) === 'id-t2_') break;
13578 if (modules['userHighlight'].coloredUsernames[idClass]) return;
13581 var hash = 5381, str = idClass;
13582 for (var i = 0; i < str.length; i++) {
13583 hash = ((hash << 5) + hash) + str.charCodeAt(i); /* hash * 33 + c */
13586 var r = (hash & 0xFF0000) >> 16;
13587 var g = (hash & 0x00FF00) >> 8;
13588 var b = hash & 0x0000FF;
13589 var color = "rgb(" + [ r, g, b ].join(',') + ")";
13592 modules['userHighlight'].doTextColor('.' + idClass, color);
13595 coloredUsernames: {},
13596 highlightUser: function (username) {
13597 var name = 'author[href$="/' + username + '"]'; // yucky, but it'll do
13598 return this.doHighlight('user', name);
13600 doHighlight: function(name, selector, container) {
13601 if (selector == undefined) {
13604 if (container == undefined) {
13607 var color, hoverColor;
13608 var color = this.colorTable[name].color;
13609 if (color === 'default') this.colorTable[name].default;
13611 var hoverColor = this.colorTable[name].hoverColor;
13612 if (hoverColor === 'default') hoverColor = '#aaa';
13614 ' + container + ' .author.' + selector + ' { \
13615 color: ' + this.options.fontColor.value + ' !important; \
13616 font-weight: bold; \
13617 padding: 0 2px 0 2px; \
13618 border-radius: 3px; \
13619 background-color:' + color + ' !important; \
13621 ' + container + ' .collapsed .author.' + selector + ' { \
13622 color: white !important; \
13623 background-color: #AAA !important; \
13625 ' + container + ' .author.' + selector + ':hover {\
13626 background-color: ' + hoverColor + ' !important; \
13627 text-decoration: none !important; \
13629 return RESUtils.addCSS(css);
13631 doTextColor: function (selector, color) {
13633 .tagline .author' + selector + ' { \
13634 color: ' + color + ' !important; \
13637 return RESUtils.addCSS(css);
13641 modules['styleTweaks'] = {
13642 moduleID: 'styleTweaks',
13643 moduleName: 'Style Tweaks',
13645 description: 'Provides a number of style tweaks to the Reddit interface',
13650 description: 'Moves the username navbar to the top (great on netbooks!)'
13655 description: 'Highlights comment boxes for easier reading / placefinding in large threads.'
13657 /* REMOVED for performance reasons...
13658 commentBoxShadows: {
13661 description: 'Drop shadows on comment boxes (turn off for faster performance)'
13667 description: 'Round corners of comment boxes'
13669 commentHoverBorder: {
13672 description: 'Highlight comment box hierarchy on hover (turn off for faster performance)'
13677 description: 'Indent comments by [x] pixels (only enter the number, no \'px\')'
13682 description: 'Show comment continuity lines'
13687 description: 'Enable lightswitch (toggle between light / dark reddit)'
13692 { name: 'Light', value: 'light' },
13693 { name: 'Dark', value: 'dark' }
13696 description: 'Light, or dark?'
13701 description: 'Reddit makes it so no links on comment pages appear as "visited" - including user profiles. This option undoes that.'
13706 description: 'Bring back video and text expando buttons for users with compressed link display'
13711 description: 'Hide vote arrows on threads where you cannot vote (e.g. archived due to age)'
13713 colorBlindFriendly: {
13716 description: 'Use colorblind friendly styles when possible'
13718 scrollSubredditDropdown: {
13721 description: 'Scroll the standard subreddit dropdown (useful for pinned header and disabled Subreddit Manager)'
13724 isEnabled: function() {
13725 return RESConsole.getModulePrefs(this.moduleID);
13728 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
13730 isMatchURL: function() {
13731 return RESUtils.isMatchURL(this.moduleID);
13733 beforeLoad: function() {
13734 if ((this.isEnabled()) && (this.isMatchURL())) {
13735 if (RESUtils.currentSubreddit()) {
13736 this.curSubReddit = RESUtils.currentSubreddit().toLowerCase();
13739 this.styleCBName = RESUtils.randomHash();
13740 RESUtils.addCSS('body.res .side .spacer .titlebox div #'+this.styleCBName+':before { display: none !important; }');
13741 RESUtils.addCSS('body.res .side .spacer .titlebox div #label-'+this.styleCBName+':before { display: none !important; }');
13742 RESUtils.addCSS('body.res .side .spacer .titlebox div #'+this.styleCBName+':after { display: none !important; }');
13743 RESUtils.addCSS('body.res .side .spacer .titlebox div #label-'+this.styleCBName+':after { display: none !important; }');
13745 // In firefox, we need to style tweet expandos because they can't take advantage of twitter.com's widget.js
13746 if (BrowserDetect.isFirefox()) {
13747 RESUtils.addCSS('.res blockquote.twitter-tweet { padding: 15px; border-left: 5px solid #ccc; font-size: 14px; line-height: 20px; }');
13748 RESUtils.addCSS('.res blockquote.twitter-tweet p { margin-bottom: 15px; }');
13751 if (this.options.colorBlindFriendly.value) {
13752 document.html.classList.add('res-colorblind');
13754 // if night mode is enabled, set a localstorage token so that in the future,
13755 // we can add the res-nightmode class to the page prior to page load.
13756 if (this.options.lightOrDark.value === 'dark') {
13757 localStorage.setItem('RES_nightMode', true);
13760 // wow, Reddit doesn't define a visited class for any links on comments pages...
13761 // let's put that back if users want it back.
13762 // If not, we still need a visited class for links in comments, like imgur photos for example, or inline image viewer can't make them look different when expanded!
13763 if (this.options.visitedStyle.value) {
13764 RESUtils.addCSS(".comment a:visited { color:#551a8b }");
13766 RESUtils.addCSS(".comment .md p > a:visited { color:#551a8b }");
13768 if (this.options.showExpandos.value) {
13769 RESUtils.addCSS('.compressed .expando-button { display: block !important; }');
13771 if ((this.options.commentBoxes.value) && (RESUtils.pageType() === 'comments')) {
13772 this.commentBoxes();
13774 if (this.options.hideUnvotable.value) {
13775 RESUtils.addCSS('.unvoted .arrow[onclick*=unvotable] { visibility: hidden }');
13776 RESUtils.addCSS('.voted .arrow[onclick*=unvotable] { cursor: normal; }');
13781 if ((this.isEnabled()) && (this.isMatchURL())) {
13783 // get the head ASAP!
13784 this.head = document.getElementsByTagName("head")[0];
13786 // handle night mode scenarios (check if subreddit is compatible, etc)
13787 this.handleNightModeAtStart();
13789 // get rid of antequated option we've removed (err, renamed) due to performance issues.
13790 if (typeof this.options.commentBoxHover !== 'undefined') {
13791 delete this.options.commentBoxHover;
13792 RESStorage.setItem('RESoptions.styleTweaks', JSON.stringify(modules['styleTweaks'].options));
13794 if (this.options.lightOrDark.value === 'dark') {
13795 // still add .res-nightmode to body just in case subreddit stylesheets specified body.res-nightmode instead of just .res-nightmode
13796 document.body.classList.add('res-nightmode');
13798 if (this.options.navTop.value) {
13801 if (this.options.lightSwitch.value) {
13802 this.lightSwitch();
13804 if (this.options.colorBlindFriendly.value) {
13805 var orangered = document.body.querySelector('#mail');
13806 if ((orangered) && (orangered.classList.contains('havemail'))) {
13807 orangered.setAttribute('style','background-image: url(http://thumbs.reddit.com/t5_2s10b_5.png); background-position: 0 0;');
13810 if (this.options.scrollSubredditDropdown.value) {
13811 var calcedHeight = Math.floor(window.innerHeight * 0.95);
13812 if( $('.drop-choices.srdrop').height() > calcedHeight ) {
13813 RESUtils.addCSS('.drop-choices.srdrop { \
13814 overflow-y:scroll; \
13815 max-height:' + calcedHeight + 'px; \
13819 if (this.options.showExpandos.value) {
13820 RESUtils.addCSS('.compressed .expando-button { display: block !important; }');
13821 var twitterLinks = document.body.querySelectorAll('.entry > p.title > a.title');
13822 var isTwitterLink = /twitter.com\/(?:#!\/)?([\w]+)\/(status|statuses)\/([\d]+)/i;
13823 for (var i=0, len = twitterLinks.length; i<len; i++) {
13824 var thisHref = twitterLinks[i].getAttribute('href');
13825 thisHref = thisHref.replace('/#!','');
13826 if (isTwitterLink.test(thisHref)) {
13827 var thisExpandoButton = document.createElement('div');
13828 thisExpandoButton.setAttribute('class','expando-button collapsed collapsedExpando selftext twitter');
13829 thisExpandoButton.addEventListener('click',modules['styleTweaks'].toggleTweetExpando,false);
13830 insertAfter(twitterLinks[i].parentNode, thisExpandoButton);
13834 this.userbarHider();
13835 this.subredditStyles();
13838 handleNightModeAtStart: function() {
13839 this.nightModeWhitelist = [];
13840 var getWhitelist = RESStorage.getItem('RESmodules.styleTweaks.nightModeWhitelist');
13841 if (getWhitelist) {
13842 this.nightModeWhitelist = safeJSON.parse(getWhitelist, 'RESmodules.styleTweaks.nightModeWhitelist');
13844 var idx = this.nightModeWhitelist.indexOf(this.curSubReddit);
13846 // go no further. this subreddit is whitelisted.
13850 // check the sidebar for a link [](#/RES_SR_Config/NightModeCompatible) that indicates the sub is night mode compatible.
13851 this.isNightmodeCompatible = (document.querySelector('.side a[href="#/RES_SR_Config/NightModeCompatible"]') !== null);
13853 // if night mode is on and the sub isn't compatible, disable its stylesheet.
13854 if (this.isDark && !this.isNightmodeCompatible) {
13855 this.disableSubredditStyle();
13859 toggleTweetExpando: function(e) {
13860 var thisExpando = e.target.nextSibling.nextSibling.nextSibling;
13861 if (e.target.classList.contains('collapsedExpando')) {
13862 $(e.target).removeClass('collapsedExpando collapsed').addClass('expanded');
13863 if (thisExpando.classList.contains('twitterLoaded')) {
13864 thisExpando.style.display = 'block';
13867 var twitterLink = e.target.previousSibling.querySelector('.title');
13868 if (twitterLink) twitterLink = twitterLink.getAttribute('href').replace('/#!','');
13869 var match = twitterLink.match(/twitter.com\/[^\/]+\/(?:status|statuses)\/([\d]+)/i);
13870 if (match !== null) {
13871 // var jsonURL = 'http://api.twitter.com/1/statuses/show/'+match[1]+'.json';
13872 var jsonURL = 'http://api.twitter.com/1/statuses/oembed.json?id='+match[1];
13873 if (BrowserDetect.isChrome()) {
13874 // we've got chrome, so we need to hit up the background page to do cross domain XHR
13876 requestType: 'loadTweet',
13879 chrome.extension.sendMessage(thisJSON, function(response) {
13880 // send message to background.html
13881 var tweet = response;
13882 $(thisExpando).html(tweet.html);
13883 thisExpando.style.display = 'block';
13884 thisExpando.classList.add('twitterLoaded');
13886 } else if (BrowserDetect.isSafari()) {
13887 // we've got safari, so we need to hit up the background page to do cross domain XHR
13888 modules['styleTweaks'].tweetExpando = thisExpando;
13890 requestType: 'loadTweet',
13893 safari.self.tab.dispatchMessage("loadTweet", thisJSON);
13894 } else if (BrowserDetect.isOpera()) {
13895 // we've got opera, so we need to hit up the background page to do cross domain XHR
13896 modules['styleTweaks'].tweetExpando = thisExpando;
13898 requestType: 'loadTweet',
13901 opera.extension.postMessage(JSON.stringify(thisJSON));
13902 } else if (BrowserDetect.isFirefox()) {
13903 // we've got a jetpack extension, hit up the background page...
13904 // we have to omit the script tag and all of the nice formatting it brings us in Firefox
13905 // because AMO does not permit externally hosted script tags being pulled in from
13906 // oEmbed like this...
13907 jsonURL += '&omit_script=true';
13908 modules['styleTweaks'].tweetExpando = thisExpando;
13910 requestType: 'loadTweet',
13913 self.postMessage(thisJSON);
13915 GM_xmlhttpRequest({
13918 target: thisExpando,
13919 onload: function(response) {
13920 var tweet = JSON.parse(response.responseText);
13921 $(thisExpando).html('<form class="usertext"><div class="usertext-body"><div class="md"><div><img style="display: block;" src="'+escapeHTML(tweet.user.profile_image_url)+'"></div>' + escapeHTML(tweet.user.screen_name) + ': ' + escapeHTML(tweet.text) + '</div></div></form>');
13922 thisExpando.style.display = 'block';
13928 $(e.target).removeClass('expanded').addClass('collapsedExpando').addClass('collapsed');
13929 thisExpando.style.display = 'none';
13933 navTop: function() {
13934 RESUtils.addCSS('#header-bottom-right { top: 19px; border-radius: 0 0 0 3px; bottom: auto; }');
13935 RESUtils.addCSS('.beta-notice { top: 48px; }');
13936 $('body, #header-bottom-right').addClass('res-navTop');
13938 userbarHider: function() {
13939 RESUtils.addCSS("#userbarToggle { min-height: 22px; position: absolute; top: auto; bottom: 0; left: -5px; width: 16px; padding-right: 3px; height: 100%; font-size: 15px; border-radius: 4px 0; color: #a1bcd6; display: inline-block; background-color: #dfecf9; border-right: 1px solid #cee3f8; cursor: pointer; text-align: right; line-height: 24px; }");
13940 RESUtils.addCSS("#userbarToggle.userbarShow { min-height: 26px; }");
13941 RESUtils.addCSS("#header-bottom-right .user { margin-left: 16px; }");
13942 // RESUtils.addCSS(".userbarHide { background-position: 0 -137px; }");
13943 RESUtils.addCSS("#userbarToggle.userbarShow { left: -12px; }");
13944 RESUtils.addCSS(".res-navTop #userbarToggle.userbarShow { top: 0; bottom: auto; }");
13945 this.userbar = document.getElementById('header-bottom-right');
13946 if (this.userbar) {
13947 this.userbarToggle = createElementWithID('div','userbarToggle');
13948 $(this.userbarToggle).html('»');
13949 this.userbarToggle.setAttribute('title','Toggle Userbar');
13950 this.userbarToggle.classList.add('userbarHide');
13951 this.userbarToggle.addEventListener('click', function(e) {
13952 modules['styleTweaks'].toggleUserBar();
13954 this.userbar.insertBefore(this.userbarToggle, this.userbar.firstChild);
13955 // var currHeight = $(this.userbar).height();
13956 // $(this.userbarToggle).css('height',currHeight+'px');
13957 if (RESStorage.getItem('RESmodules.styleTweaks.userbarState') === 'hidden') {
13958 this.toggleUserBar();
13962 toggleUserBar: function() {
13963 var nextEle = this.userbarToggle.nextSibling;
13965 if (this.userbarToggle.classList.contains('userbarHide')) {
13966 this.userbarToggle.classList.remove('userbarHide');
13967 this.userbarToggle.classList.add('userbarShow');
13968 $(this.userbarToggle).html('«');
13969 RESStorage.setItem('RESmodules.styleTweaks.userbarState', 'hidden');
13970 modules['accountSwitcher'].closeAccountMenu();
13971 while ((typeof nextEle !== 'undefined') && (nextEle !== null)) {
13972 nextEle.style.display = 'none';
13973 nextEle = nextEle.nextSibling;
13977 this.userbarToggle.classList.remove('userbarShow');
13978 this.userbarToggle.classList.add('userbarHide');
13979 $(this.userbarToggle).html('»');
13980 RESStorage.setItem('RESmodules.styleTweaks.userbarState', 'visible');
13981 while ((typeof nextEle !== 'undefined') && (nextEle !== null)) {
13982 if ((/mail/.test(nextEle.className)) || (nextEle.id === 'openRESPrefs')) {
13983 nextEle.style.display = 'inline-block';
13985 nextEle.style.display = 'inline';
13987 nextEle = nextEle.nextSibling;
13991 commentBoxes: function() {
13992 document.html.classList.add('res-commentBoxes');
13993 if (this.options.commentRounded.value) {
13994 document.html.classList.add('res-commentBoxes-rounded');
13996 if (this.options.continuity.value) {
13997 document.html.classList.add('res-continuity');
13999 if (this.options.commentHoverBorder.value) {
14000 document.html.classList.add('res-commentHoverBorder');
14002 if (this.options.commentIndent.value) {
14003 // this should override the default of 10px in commentboxes.css because it's added later.
14004 RESUtils.addCSS('.res-commentBoxes .comment { margin-left:'+this.options.commentIndent.value+'px !important; }');
14007 lightSwitch: function() {
14008 RESUtils.addCSS(".lightOn { background-position: 0 -96px; } ");
14009 RESUtils.addCSS(".lightOff { background-position: 0 -108px; } ");
14010 var thisFrag = document.createDocumentFragment();
14011 this.lightSwitch = document.createElement('li');
14012 this.lightSwitch.setAttribute('title',"Toggle night and day");
14013 this.lightSwitch.addEventListener('click', function(e) {
14014 e.preventDefault();
14015 if (modules['styleTweaks'].isDark == true) {
14016 RESUtils.setOption('styleTweaks','lightOrDark','light');
14017 modules['styleTweaks'].lightSwitchToggle.classList.remove('enabled');
14018 modules['styleTweaks'].redditDark(true);
14020 RESUtils.setOption('styleTweaks','lightOrDark','dark');
14021 modules['styleTweaks'].lightSwitchToggle.classList.add('enabled');
14022 modules['styleTweaks'].redditDark();
14025 // this.lightSwitch.setAttribute('id','lightSwitch');
14026 this.lightSwitch.textContent = 'night mode';
14027 this.lightSwitchToggle = createElementWithID('div','lightSwitchToggle','toggleButton');
14028 $(this.lightSwitchToggle).html('<span class="toggleOn">on</span><span class="toggleOff">off</span>');
14029 this.lightSwitch.appendChild(this.lightSwitchToggle);
14030 (this.options.lightOrDark.value === 'dark') ? this.lightSwitchToggle.classList.add('enabled') : this.lightSwitchToggle.classList.remove('enabled');
14031 // thisFrag.appendChild(separator);
14032 thisFrag.appendChild(this.lightSwitch);
14033 // if (RESConsole.RESPrefsLink) insertAfter(RESConsole.RESPrefsLink, thisFrag);
14034 $('#RESDropdownOptions').append(this.lightSwitch);
14036 subredditStyles: function() {
14037 if (! RESUtils.currentSubreddit()) return;
14038 this.ignoredSubReddits = [];
14039 var getIgnored = RESStorage.getItem('RESmodules.styleTweaks.ignoredSubredditStyles');
14041 this.ignoredSubReddits = safeJSON.parse(getIgnored, 'RESmodules.styleTweaks.ignoredSubredditStyles');
14043 var subredditTitle = document.querySelector('.titlebox h1');
14044 this.styleToggleContainer = document.createElement('div');
14045 this.styleToggleLabel = document.createElement('label');
14046 this.styleToggleCheckbox = document.createElement('input');
14047 this.styleToggleCheckbox.setAttribute('type','checkbox');
14048 this.styleToggleCheckbox.setAttribute('id',this.styleCBName);
14049 this.styleToggleCheckbox.setAttribute('name',this.styleCBName);
14051 // are we blacklisting, or whitelisting subreddits? If we're in night mode on a sub that's
14052 // incompatible with it, we want to check the whitelist. Otherwise, check the blacklist.
14054 if ((this.curSubReddit !== null) && (subredditTitle !== null)) {
14056 if (this.isDark && !this.isNightmodeCompatible) {
14057 var idx = this.nightModeWhitelist.indexOf(this.curSubReddit);
14059 this.styleToggleCheckbox.checked = true;
14062 var idx = this.ignoredSubReddits.indexOf(this.curSubReddit);
14064 this.styleToggleCheckbox.checked = true;
14066 this.toggleSubredditStyle(false);
14069 this.styleToggleCheckbox.addEventListener('change', function(e) {
14070 modules['styleTweaks'].toggleSubredditStyle(this.checked);
14072 this.styleToggleContainer.appendChild(this.styleToggleCheckbox);
14073 insertAfter(subredditTitle, this.styleToggleContainer);
14075 this.styleToggleLabel.setAttribute('for',this.styleCBName);
14076 this.styleToggleLabel.setAttribute('id','label-'+this.styleCBName);
14077 this.styleToggleLabel.textContent = 'Use subreddit style ';
14078 this.styleToggleContainer.appendChild(this.styleToggleLabel);
14079 this.setSRStyleToggleVisibility(true); // no source
14081 srstyleHideLock: RESUtils.createMultiLock(),
14082 setSRStyleToggleVisibility: function (visible, source) {
14083 /// When showing/hiding popups which could overlay the "Use subreddit style" checkbox,
14084 /// set the checkbox's styling to "less visible" or "more visible"
14085 /// @param visible bool make checkbox "more visible" (true) or less (false)
14086 /// @param source string popup ID, so checkbox stays less visible until that popup's lock is released
14087 var self = modules['styleTweaks'];
14088 if (!self.styleToggleContainer) return;
14090 if (typeof source !== "undefined") {
14092 self.srstyleHideLock.unlock(source);
14094 self.srstyleHideLock.lock(source);
14098 if (visible && self.srstyleHideLock.locked()) {
14102 // great, now people are still finding ways to hide this.. these extra declarations are to try and fight that.
14103 // Sorry, subreddit moderators, but users can disable all subreddit stylesheets if they want - this is a convenience
14104 // for them and I think taking this functionality away from them is unacceptable.
14106 var zIndex = 'z-index: ' + (visible ? ' 2147483647' : 'auto') + ' !important;';
14108 self.styleToggleContainer.setAttribute( 'style', 'margin: 0 !important; background-color: inherit !important; color: inherit !important; display: block !important; position: relative !important; left: 0 !important; top: 0 !important; max-height: none!important; max-width: none!important; height: auto !important; width: auto !important; visibility: visible !important; overflow: auto !important; text-indent: 0 !important; font-size: 12px !important; float: none !important; opacity: 1 !important;' + zIndex );
14109 self.styleToggleCheckbox.setAttribute( 'style', 'margin: 0 !important; background-color: inherit !important; color: inherit !important; display: inline-block !important; position: relative !important; left: 0 !important; top: 0 !important; max-height: none!important; max-width: none!important; height: auto !important; width: auto !important; visibility: visible !important; overflow: auto !important; text-indent: 0 !important; font-size: 12px !important; float: none !important; opacity: 1 !important;' + zIndex );
14110 self.styleToggleLabel.setAttribute( 'style', 'margin: 0 !important; background-color: inherit !important; color: inherit !important; display: inline-block !important; position: relative !important; left: 0 !important; top: 0 !important; max-height: none!important; max-width: none!important; height: auto !important; width: auto !important; visibility: visible !important; overflow: auto !important; text-indent: 0 !important; font-size: 12px !important; margin-left: 4px !important; float: none !important; opacity: 1 !important;' + zIndex );
14112 toggleSubredditStyle: function(toggle, subreddit) {
14113 var togglesr = (subreddit) ? subreddit.toLowerCase() : this.curSubReddit;
14115 this.enableSubredditStyle(subreddit);
14117 this.disableSubredditStyle(subreddit);
14120 enableSubredditStyle: function(subreddit) {
14121 var togglesr = (subreddit) ? subreddit.toLowerCase() : this.curSubReddit;
14123 if (this.isDark && !this.isNightmodeCompatible) {
14124 var idx = this.nightModeWhitelist.indexOf(togglesr);
14125 if (idx === -1) this.nightModeWhitelist.push(togglesr); // add if not found
14126 RESStorage.setItem('RESmodules.styleTweaks.nightModeWhitelist',JSON.stringify(this.nightModeWhitelist));
14127 } else if (this.ignoredSubReddits) {
14128 var idx = this.ignoredSubReddits.indexOf(togglesr);
14129 if (idx !== -1) this.ignoredSubReddits.splice(idx, 1); // Remove it if found...
14130 RESStorage.setItem('RESmodules.styleTweaks.ignoredSubredditStyles',JSON.stringify(this.ignoredSubReddits));
14133 var subredditStyleSheet = document.createElement('link');
14134 subredditStyleSheet.setAttribute('title','applied_subreddit_stylesheet');
14135 subredditStyleSheet.setAttribute('rel','stylesheet');
14136 subredditStyleSheet.setAttribute('href','http://www.reddit.com/r/'+togglesr+'/stylesheet.css');
14137 if (!subreddit || (subreddit == this.curSubReddit)) this.head.appendChild(subredditStyleSheet);
14139 disableSubredditStyle: function(subreddit) {
14140 var togglesr = (subreddit) ? subreddit.toLowerCase() : this.curSubReddit;
14142 if (this.isDark && !this.isNightmodeCompatible) {
14143 var idx = this.nightModeWhitelist.indexOf(togglesr);
14144 if (idx !== -1) this.nightModeWhitelist.splice(idx, 1); // Remove it if found...
14145 RESStorage.setItem('RESmodules.styleTweaks.nightModeWhitelist',JSON.stringify(this.nightModeWhitelist));
14146 } else if (this.ignoredSubReddits) {
14147 var idx = this.ignoredSubReddits.indexOf(togglesr); // Find the index
14148 if (idx === -1) this.ignoredSubReddits.push(togglesr);
14149 RESStorage.setItem('RESmodules.styleTweaks.ignoredSubredditStyles',JSON.stringify(this.ignoredSubReddits));
14152 var subredditStyleSheet = this.head.querySelector('link[title=applied_subreddit_stylesheet]');
14153 if (!subredditStyleSheet) subredditStyleSheet = this.head.querySelector('style[title=applied_subreddit_stylesheet]');
14154 if ((subredditStyleSheet) && (!subreddit || (subreddit == this.curSubReddit))) {
14155 subredditStyleSheet.parentNode.removeChild(subredditStyleSheet);
14158 redditDark: function(off) {
14160 this.isDark = false;
14161 localStorage.removeItem('RES_nightMode');
14162 document.html.classList.remove('res-nightmode');
14163 if (document.body) {
14164 document.body.classList.remove('res-nightmode');
14167 this.isDark = true;
14168 localStorage.setItem('RES_nightMode', true);
14169 document.html.classList.add('res-nightmode');
14170 if (document.body) {
14171 document.body.classList.add('res-nightmode');
14177 modules['accountSwitcher'] = {
14178 moduleID: 'accountSwitcher',
14179 moduleName: 'Account Switcher',
14180 category: 'Accounts',
14184 addRowText: '+add account',
14186 { name: 'username', type: 'text' },
14187 { name: 'password', type: 'password' }
14191 ['somebodymakethis','SMT','[SMT]'],
14192 ['pics','pic','[pic]']
14195 description: 'Set your usernames and passwords below. They are only stored in RES preferences.'
14200 description: 'Keep me logged in when I restart my browser.'
14202 showCurrentUserName: {
14205 description: 'Show my current user name in the Account Switcher.'
14210 { name: 'snoo (alien)', value: 'alien' },
14211 { name: 'simple arrow', value: 'arrow' }
14214 description: 'Use the "snoo" icon, or older style dropdown?'
14217 description: 'Store username/password pairs and switch accounts instantly while browsing Reddit!',
14218 isEnabled: function() {
14219 return RESConsole.getModulePrefs(this.moduleID);
14222 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
14224 isMatchURL: function() {
14225 return RESUtils.isMatchURL(this.moduleID);
14227 beforeLoad: function() {
14228 if ((this.isEnabled()) && (this.isMatchURL())) {
14229 RESUtils.addCSS('#header-bottom-right { height: auto; padding: 4px 4px 7px }')
14230 RESUtils.addCSS('#RESAccountSwitcherDropdown { min-width: 110px; width: auto; display: none; position: absolute; z-index: 1000; }');
14231 RESUtils.addCSS('#RESAccountSwitcherDropdown li { height: auto; line-height: 20px; padding: 2px 10px; }');
14232 if (this.options.dropDownStyle.value === 'alien') {
14233 RESUtils.addCSS('#RESAccountSwitcherIcon { cursor: pointer; margin-left: 3px; display: inline-block; width: 12px; vertical-align: middle; height: 16px; background-repeat: no-repeat; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAPCAYAAAAyPTUwAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkQ3NTExRkExOEYzNTExRTFBNjgzQzhEOUY2QzU2MUNFIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkQ3NTExRkEyOEYzNTExRTFBNjgzQzhEOUY2QzU2MUNFIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RDc1MTFGOUY4RjM1MTFFMUE2ODNDOEQ5RjZDNTYxQ0UiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RDc1MTFGQTA4RjM1MTFFMUE2ODNDOEQ5RjZDNTYxQ0UiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6W3fJJAAAB4ElEQVR42mJgwA4YgdgSiJUUFRXDW1tbL7Kzswsw4VDMBcRXgfgeMzPzJx4eHn4gG0MtSICPjY3NF0jLoCtglJWV1eDm5rZmZWX9k5ZbWGFmYqwhwM3B8Pn7T4bzl6/enzNlQsfrV68+srKxPWHMz89/ZmJiIunn58fA9+YKAwMHHwODlA4Dw4fHDAzPbzD8VLRhWLNuPcOzp0//MEhJSaU/f/HyPxhkyf//3xsEYa+s/f8/nOn//19f/n/98fO/jo5ONwMfH5/S27dvwfL/nt/5//8rhP3/z7f//55cgzD//PkPdK4F2N3x8fFLv3///v/d56//l69a83///v3/V65e8//+k+f///79+7+4uPgAUB0zIywUgNZEZmVlzRMTE2P78OEDA9DTDN++ffs3c+bMglOnTk0HqvkDC5p/L168+P7582cmaWlpBhUVFQZ5eXkGoPUMDx8+BMn/QQ5C1vb29r+HDx/+jwwuXLjwv7e39z8wWHkYkAOdk5OT4cePHygx9OXLF7BzgPpQo05NTS2mp6fnO7LJc+bM+a2np1eKNUFISEg0gEIFHIz//v3X1dWdDU1UYMAMYzg7O8eUlpYmXLly5dtfFm6h40cO3DU2NhYBphOea9euHQOpAQgwAKMW+Z5mJFvIAAAAAElFTkSuQmCC); }');
14234 RESUtils.addCSS('#RESAccountSwitcherIconOverlay { cursor: pointer; position: absolute; display: none; width: 11px; height: 22px; background-position: 2px 3px; padding-left: 2px; padding-right: 2px; padding-top: 3px; border: 1px solid #369; border-bottom: 1px solid #5f99cf; background-color: #5f99cf; border-radius: 3px 3px 0 0; z-index: 100; background-repeat: no-repeat; background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAAPCAYAAAAyPTUwAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyBpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMC1jMDYwIDYxLjEzNDc3NywgMjAxMC8wMi8xMi0xNzozMjowMCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNSBXaW5kb3dzIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkQ3NTExRkExOEYzNTExRTFBNjgzQzhEOUY2QzU2MUNFIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkQ3NTExRkEyOEYzNTExRTFBNjgzQzhEOUY2QzU2MUNFIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6RDc1MTFGOUY4RjM1MTFFMUE2ODNDOEQ5RjZDNTYxQ0UiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6RDc1MTFGQTA4RjM1MTFFMUE2ODNDOEQ5RjZDNTYxQ0UiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz6W3fJJAAAB4ElEQVR42mJgwA4YgdgSiJUUFRXDW1tbL7Kzswsw4VDMBcRXgfgeMzPzJx4eHn4gG0MtSICPjY3NF0jLoCtglJWV1eDm5rZmZWX9k5ZbWGFmYqwhwM3B8Pn7T4bzl6/enzNlQsfrV68+srKxPWHMz89/ZmJiIunn58fA9+YKAwMHHwODlA4Dw4fHDAzPbzD8VLRhWLNuPcOzp0//MEhJSaU/f/HyPxhkyf//3xsEYa+s/f8/nOn//19f/n/98fO/jo5ONwMfH5/S27dvwfL/nt/5//8rhP3/z7f//55cgzD//PkPdK4F2N3x8fFLv3///v/d56//l69a83///v3/V65e8//+k+f///79+7+4uPgAUB0zIywUgNZEZmVlzRMTE2P78OEDA9DTDN++ffs3c+bMglOnTk0HqvkDC5p/L168+P7582cmaWlpBhUVFQZ5eXkGoPUMDx8+BMn/QQ5C1vb29r+HDx/+jwwuXLjwv7e39z8wWHkYkAOdk5OT4cePHygx9OXLF7BzgPpQo05NTS2mp6fnO7LJc+bM+a2np1eKNUFISEg0gEIFHIz//v3X1dWdDU1UYMAMYzg7O8eUlpYmXLly5dtfFm6h40cO3DU2NhYBphOea9euHQOpAQgwAKMW+Z5mJFvIAAAAAElFTkSuQmCC); }');
14236 RESUtils.addCSS('#RESAccountSwitcherIcon { display: inline-block; vertical-align: middle; margin-left: 3px; }');
14237 RESUtils.addCSS('#RESAccountSwitcherIcon .downArrow { cursor: pointer; margin-top: 2px; display: block; width: 16px; height: 10px; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); background-position: 0 -106px; }');
14238 RESUtils.addCSS('#RESAccountSwitcherIconOverlay { cursor: pointer; position: absolute; display: none; width: 20px; height: 22px; z-index: 100; border: 1px solid #369; border-bottom: 1px solid #5f99cf; background-color: #5f99cf; border-radius: 3px 3px 0 0; }');
14239 RESUtils.addCSS('#RESAccountSwitcherIconOverlay .downArrow { margin-top: 6px; margin-left: 3px; display: inline-block; width: 18px; height: 10px; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); background-position: 0 -96px; }');
14240 // this.alienIMG = '<span class="downArrow"></span>';
14242 // RESUtils.addCSS('#RESAccountSwitcherIconOverlay { display: none; position: absolute; }');
14246 if ((this.isEnabled()) && (this.isMatchURL())) {
14247 this.userLink = document.querySelector('#header-bottom-right > span.user > a');
14248 if (this.userLink) {
14249 this.userLink.style.marginRight = '2px';
14250 this.loggedInUser = RESUtils.loggedInUser();
14251 // var downArrowIMG = 'data:image/gif;base64,R0lGODlhBwAEALMAAAcHBwgICAoKChERETs7Ozo6OkJCQg0NDRoaGhAQEAwMDDIyMv///wAAAAAAAAAAACH5BAEAAAwALAAAAAAHAAQAAAQQ0BSykADsDAUwY4kQfOT4RQA7';
14252 if (this.options.dropDownStyle.value === 'alien') {
14253 this.downArrowOverlay = $('<span id="RESAccountSwitcherIconOverlay"></span>');
14254 this.downArrow = $('<span id="RESAccountSwitcherIcon"></span>');
14256 this.downArrowOverlay = $('<span id="RESAccountSwitcherIconOverlay"><span class="downArrow"></span></span>');
14257 this.downArrow = $('<span id="RESAccountSwitcherIcon"><span class="downArrow"></span></span>');
14259 this.downArrowOverlay.on('click', function() {
14260 modules['accountSwitcher'].toggleAccountMenu(false);
14261 modules['accountSwitcher'].manageAccounts();
14262 }).appendTo(document.body);
14264 this.downArrow.on('click', function() {
14265 modules['accountSwitcher'].updateUserDetails();
14266 modules['accountSwitcher'].toggleAccountMenu(true);
14268 this.downArrowOverlay.on('mouseleave', function() {
14269 modules['accountSwitcher'].dropdownTimer = setTimeout(function() {
14270 modules['accountSwitcher'].toggleAccountMenu(false);
14274 // insertAfter(this.userLink, downArrow);
14275 $(this.userLink).after(this.downArrow);
14277 this.accountMenu = $('<ul id="RESAccountSwitcherDropdown" class="RESDropdownList"></ul>')
14278 this.accountMenu.on('mouseenter', function() {
14279 clearTimeout(modules['accountSwitcher'].dropdownTimer);
14281 this.accountMenu.on('mouseleave', function() {
14282 modules['accountSwitcher'].toggleAccountMenu(false);
14284 // GM_addStyle(css);
14285 var accounts = this.options.accounts.value;
14286 if (accounts !== null) {
14287 var accountCount = 0;
14288 for (var i=0, len=accounts.length; i<len; i++) {
14289 var thisPair = accounts[i],
14290 username = thisPair[0];
14291 if (!this.loggedInUser || username.toUpperCase() !== this.loggedInUser.toUpperCase() || this.options.showCurrentUserName.value){
14293 var $accountLink = $('<li>', { 'class': 'accountName' });
14294 // Check if user is logged in before comparing
14295 if (this.loggedInUser && username.toUpperCase() === this.loggedInUser.toUpperCase()) {
14296 $accountLink.addClass('active');
14300 .data('username', username)
14302 .css('cursor', 'pointer')
14303 .on('click', function(e) {
14304 e.preventDefault();
14305 modules['accountSwitcher'].switchTo($(this).data('username'));
14307 .appendTo(this.accountMenu);
14309 RESUtils.getUserInfo(function (userInfo) {
14310 var userDetails = username;
14312 // Display the karma of the user, if it is already pre-fetched
14313 if (userInfo && !userInfo.error && userInfo.data) {
14314 userDetails = username + ' (' + userInfo.data.link_karma + ' · ' + userInfo.data.comment_karma + ')';
14317 $accountLink.html(userDetails);
14318 }, username, false);
14321 $('<li>', { 'class': 'addAccount' })
14322 .text('+ add account')
14323 .css('cursor', 'pointer')
14324 .on('click', function(e) {
14325 e.preventDefault();
14326 modules['accountSwitcher'].toggleAccountMenu(false);
14327 modules['accountSwitcher'].manageAccounts();
14329 .appendTo(this.accountMenu);
14331 $(document.body).append(this.accountMenu);
14335 updateUserDetails: function() {
14336 this.accountMenu.find('.accountName').each(function (index) {
14337 var username = $(this).data('username'),
14340 // Ignore "+ add account"
14341 if (typeof username === 'undefined') {
14345 // Leave a 500 ms delay between requests
14346 setTimeout(function () {
14347 RESUtils.getUserInfo(function (userInfo) {
14348 // Fail if retrieving the user's info results in an error (such as a 404)
14349 if (!userInfo || userInfo.error || !userInfo.data) {
14353 // Display the karma of the user
14354 var userDetails = username + ' (' + userInfo.data.link_karma + ' · ' + userInfo.data.comment_karma + ')';
14355 $(that).html(userDetails);
14360 toggleAccountMenu: function(open) {
14361 if ((open) || (! $(modules['accountSwitcher'].accountMenu).is(':visible'))) {
14362 var thisHeight = 18;
14363 if ($('#RESAccountSwitcherDropdown').css('position') !== 'fixed') {
14364 var thisX = $(modules['accountSwitcher'].userLink).offset().left;
14365 var thisY = $(modules['accountSwitcher'].userLink).offset().top;
14367 var thisX = $('#header-bottom-right').position().left + $(modules['accountSwitcher'].userLink).position().left;
14368 var thisY = $(modules['accountSwitcher'].userLink).position().top;
14369 if (modules['betteReddit'].options.pinHeader.value === 'subanduser') {
14370 thisHeight += $('#sr-header-area').height();
14371 } else if (modules['betteReddit'].options.pinHeader.value === 'header') {
14372 thisHeight += $('#sr-header-area').height();
14375 $(modules['accountSwitcher'].accountMenu).css({
14376 top: (thisY + thisHeight) + 'px',
14377 left: (thisX) + 'px'
14379 $(modules['accountSwitcher'].accountMenu).show();
14380 var thisX = $(modules['accountSwitcher'].downArrow).offset().left;
14381 var thisY = $(modules['accountSwitcher'].downArrow).offset().top;
14382 $(modules['accountSwitcher'].downArrowOverlay).css({
14383 top: (thisY-4) + 'px',
14384 left: (thisX-3) + 'px'
14387 $(modules['accountSwitcher'].downArrowOverlay).show();
14388 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'accountSwitcher');
14391 $(modules['accountSwitcher'].accountMenu).hide();
14392 $(modules['accountSwitcher'].downArrowOverlay).hide();
14393 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'accountSwitcher');
14396 closeAccountMenu: function() {
14397 // this function basically just exists for other modules to call.
14398 this.accountMenu.hide();
14400 switchTo: function(username) {
14401 var accounts = this.options.accounts.value;
14404 if (this.options.keepLoggedIn.value) {
14407 for (var i=0, len=accounts.length; i<len; i++) {
14408 var thisPair = accounts[i];
14409 if (thisPair[0].toUpperCase() === username.toUpperCase()) {
14410 password = thisPair[1];
14414 var loginUrl = 'https://ssl.reddit.com/api/login';
14415 // unfortunately, due to 3rd party cookie issues, none of the below browsers work with ssl.
14416 if (BrowserDetect.isOpera()) {
14417 loginUrl = 'http://'+location.hostname+'/api/login';
14418 } else if ((BrowserDetect.isChrome()) && (chrome.extension.inIncognitoContext)) {
14419 loginUrl = 'http://'+location.hostname+'/api/login';
14420 } else if (BrowserDetect.isSafari()) {
14421 loginUrl = 'http://'+location.hostname+'/api/login';
14424 // Remove old session cookie
14425 RESUtils.deleteCookie('reddit_session');
14427 GM_xmlhttpRequest({
14430 data: 'user='+encodeURIComponent(username)+'&passwd='+encodeURIComponent(password)+rem,
14432 "Content-Type": "application/x-www-form-urlencoded"
14434 onload: function(response) {
14435 var badData = false;
14437 var data = JSON.parse(response.responseText);
14443 var error = /WRONG_PASSWORD/;
14444 var rateLimit = /RATELIMIT/;
14446 RESUtils.notification({
14448 moduleID: 'accountSwitcher',
14449 message: 'Could not switch accounts. Reddit may be under heavy load. Please try again in a few moments.'
14451 } else if (error.test(response.responseText)) {
14452 alert('Incorrect login and/or password. Please check your configuration.');
14453 } else if (rateLimit.test(response.responseText)) {
14454 alert('RATE LIMIT: The Reddit API is seeing too many hits from you too fast, perhaps you keep submitting a wrong password, etc? Try again in a few minutes.');
14461 manageAccounts: function() {
14462 modules['settingsNavigation'].loadSettingsPage('accountSwitcher', 'accounts');
14466 modules['filteReddit'] = {
14467 moduleID: 'filteReddit',
14468 moduleName: 'filteReddit',
14469 category: 'Filters',
14471 // any configurable options you have go here...
14472 // options must have a type and a value..
14473 // valid types are: text, boolean (if boolean, value must be true or false)
14478 description: 'Filters all links labelled NSFW'
14483 description: 'Show a notification when posts are filtered'
14488 description: 'Add a quick NSFW on/off toggle to the gear menu'
14492 addRowText: '+add filter',
14494 { name: 'keyword', type: 'text' },
14498 { name: 'Everywhere', value: 'everywhere' },
14499 { name: 'Everywhere but:', value: 'exclude' },
14500 { name: 'Only on:', value: 'include' }
14502 value: 'everywhere',
14503 description: 'Apply filter to:'
14508 source: '/api/search_reddit_names.json?app=res',
14509 hintText: 'type a subreddit name',
14510 onResult: function(response) {
14511 var names = response.names;
14513 for (var i=0, len=names.length; i<len; i++) {
14514 results.push({id: names[i], name: names[i]});
14518 onCachedResult: function(response) {
14519 var names = response.names;
14521 for (var i=0, len=names.length; i<len; i++) {
14522 results.push({id: names[i], name: names[i]});
14527 /* { name: 'inclusions', type: 'list', source: location.protocol + '/api/search_reddit_names' } */
14531 description: 'Type in title keywords you want to ignore if they show up in a title'
14535 addRowText: '+add filter',
14537 { name: 'subreddit', type: 'text' }
14541 description: 'Type in a subreddit you want to ignore (only applies to /r/all or /domain/* urls)'
14545 addRowText: '+add filter',
14547 { name: 'domain', type: 'text' },
14551 { name: 'Everywhere', value: 'everywhere' },
14552 { name: 'Everywhere but:', value: 'exclude' },
14553 { name: 'Only on:', value: 'include' }
14555 value: 'everywhere',
14556 description: 'Apply filter to:'
14561 source: '/api/search_reddit_names.json?app=res',
14562 hintText: 'type a subreddit name',
14563 onResult: function(response) {
14564 var names = response.names;
14566 for (var i=0, len=names.length; i<len; i++) {
14567 results.push({id: names[i], name: names[i]});
14571 onCachedResult: function(response) {
14572 var names = response.names;
14574 for (var i=0, len=names.length; i<len; i++) {
14575 results.push({id: names[i], name: names[i]});
14580 /* { name: 'inclusions', type: 'list', source: location.protocol + '/api/search_reddit_names' } */
14584 description: 'Type in domain keywords you want to ignore. Note that \"reddit\" would ignore \"reddit.com\" and \"fooredditbar.com\"'
14588 addRowText: '+add filter',
14590 { name: 'keyword', type: 'text' },
14594 { name: 'Everywhere', value: 'everywhere' },
14595 { name: 'Everywhere but:', value: 'exclude' },
14596 { name: 'Only on:', value: 'include' }
14598 value: 'everywhere',
14599 description: 'Apply filter to:'
14604 source: '/api/search_reddit_names.json?app=res',
14605 hintText: 'type a subreddit name',
14606 onResult: function(response) {
14607 var names = response.names;
14609 for (var i=0, len=names.length; i<len; i++) {
14610 results.push({id: names[i], name: names[i]});
14614 onCachedResult: function(response) {
14615 var names = response.names;
14617 for (var i=0, len=names.length; i<len; i++) {
14618 results.push({id: names[i], name: names[i]});
14623 /* { name: 'inclusions', type: 'list', source: location.protocol + '/api/search_reddit_names' } */
14627 description: 'Type in keywords you want to ignore if they are contained in link flair'
14631 addRowText: "+add subreddits",
14632 description: "Whitelist subreddits from NSFW filter",
14635 name: 'subreddits',
14637 source: '/api/search_reddit_names.json?app=res',
14638 hintText: 'type a subreddit name',
14639 onResult: function(response) {
14640 var names = response.names;
14642 for (var i=0, len=names.length; i<len; i++) {
14643 results.push({id: names[i], name: names[i]});
14647 onCachedResult: function(response) {
14648 var names = response.names;
14650 for (var i=0, len=names.length; i<len; i++) {
14651 results.push({id: names[i], name: names[i]});
14660 { name: 'Everywhere', value: 'everywhere' },
14661 { name: 'When browsing subreddit/multi-subreddit', value: 'visit' }
14663 value: 'everywhere'
14668 description: 'Filter out NSFW content, or links by keyword, domain (use User Tagger to ignore by user) or subreddit (for /r/all or /domain/*).',
14669 isEnabled: function() {
14670 return RESConsole.getModulePrefs(this.moduleID);
14673 /^https?:\/\/([a-z]+)\.reddit\.com\/?(?:\??[\w]+=[\w]+&?)*/i,
14674 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\w]+\/?(?:\??[\w]+=[\w]+&?)*$/i
14677 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
14678 /^https?:\/\/([a-z]+)\.reddit\.com\/saved\/?/i,
14679 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
14681 isMatchURL: function() {
14682 return RESUtils.isMatchURL(this.moduleID);
14684 beforeLoad: function() {
14685 if (this.isEnabled()) {
14686 RESUtils.addCSS('.RESFilterToggle { margin-right: 5px; color: white; background-image: url(http://www.redditstatic.com/bg-button-add.png); cursor: pointer; text-align: center; width: 68px; font-weight: bold; font-size: 10px; border: 1px solid #444; padding: 1px 6px; border-radius: 3px 3px 3px 3px; }');
14687 RESUtils.addCSS('.RESFilterToggle.remove { background-image: url(http://www.redditstatic.com/bg-button-remove.png) }');
14688 RESUtils.addCSS('.RESFiltered { display: none !important; }');
14689 if (this.options.NSFWfilter.value) {
14690 this.addNSFWFilterStyle();
14695 // shh I'm cheating. This runs the toggle on every single page, bypassing isMatchURL.
14696 if ((this.isEnabled()) && (this.options.NSFWQuickToggle.value)) {
14697 var thisFrag = document.createDocumentFragment();
14698 this.nsfwSwitch = document.createElement('li');
14699 this.nsfwSwitch.setAttribute('title',"Toggle NSFW Filter");
14700 this.nsfwSwitch.addEventListener('click', function(e) {
14701 e.preventDefault();
14702 modules['filteReddit'].toggleNsfwFilter();
14704 this.nsfwSwitch.textContent = 'nsfw filter';
14705 this.nsfwSwitchToggle = createElementWithID('div','nsfwSwitchToggle','toggleButton');
14706 $(this.nsfwSwitchToggle).html('<span class="toggleOn">on</span><span class="toggleOff">off</span>');
14707 this.nsfwSwitch.appendChild(this.nsfwSwitchToggle);
14708 (this.options.NSFWfilter.value) ? this.nsfwSwitchToggle.classList.add('enabled') : this.nsfwSwitchToggle.classList.remove('enabled');
14709 thisFrag.appendChild(this.nsfwSwitch);
14710 $('#RESDropdownOptions').append(this.nsfwSwitch);
14713 if ((this.isEnabled()) && (this.isMatchURL())) {
14714 this.scanEntries();
14715 RESUtils.watchForElement('siteTable', modules['filteReddit'].scanEntries);
14718 toggleNsfwFilter: function(toggle, notify) {
14719 if (toggle !== true && toggle === false || modules['filteReddit'].options.NSFWfilter.value == true) {
14720 modules['filteReddit'].filterNSFW(false);
14721 RESUtils.setOption('filteReddit','NSFWfilter',false);
14722 $(modules['filteReddit'].nsfwSwitchToggle).removeClass('enabled');
14724 modules['filteReddit'].filterNSFW(true);
14725 RESUtils.setOption('filteReddit','NSFWfilter',true);
14726 $(modules['filteReddit'].nsfwSwitchToggle).addClass('enabled');
14730 var onOff = modules['filteReddit'].options.NSFWfilter.value ? 'on' : ' off';
14732 RESUtils.notification({
14733 header: 'NSFW Filter',
14734 moduleID: 'filteReddit',
14735 optionKey: 'NSFWfilter',
14736 message: 'NSFW Filter has been turned ' + onOff + '.'
14740 scanEntries: function(ele) {
14741 var numFiltered = 0;
14742 var numNsfwHidden = 0;
14746 entries = document.querySelectorAll('#siteTable div.thing.link');
14748 entries = ele.querySelectorAll('div.thing.link');
14750 // var RALLre = /\/r\/all\/?(([\w]+)\/)?/i;
14751 // var onRALL = RALLre.exec(location.href);
14752 var filterSubs = (RESUtils.currentSubreddit('all')) || (RESUtils.currentDomain());
14753 for (var i=0, len=entries.length; i<len;i++) {
14754 var postTitle = entries[i].querySelector('.entry a.title').innerHTML;
14755 var postDomain = entries[i].querySelector('.entry span.domain > a').innerHTML.toLowerCase();
14756 var thisSubreddit = entries[i].querySelector('.entry a.subreddit');
14757 var postFlair = entries[i].querySelector('.entry span.linkflairlabel');
14758 if (thisSubreddit !== null) {
14759 var postSubreddit = thisSubreddit.innerHTML;
14761 var postSubreddit = false;
14763 var filtered = false;
14764 var currSub = (RESUtils.currentSubreddit()) ? RESUtils.currentSubreddit().toLowerCase() : null;
14765 filtered = modules['filteReddit'].filterTitle(postTitle, postSubreddit || RESUtils.currentSubreddit());
14766 if (!filtered) filtered = modules['filteReddit'].filterDomain(postDomain, postSubreddit || currSub);
14767 if ((!filtered) && (filterSubs) && (postSubreddit)) {
14768 filtered = modules['filteReddit'].filterSubreddit(postSubreddit);
14770 if ((!filtered) && (postFlair)) {
14771 filtered = modules['filteReddit'].filterFlair(postFlair.textContent, postSubreddit || RESUtils.currentSubreddit());
14774 entries[i].classList.add('RESFiltered')
14778 if (entries[i].classList.contains('over18')) {
14779 if (modules['filteReddit'].allowNSFW(postSubreddit, currSub)) {
14780 entries[i].classList.add('allowOver18');
14781 } else if (modules['filteReddit'].options.NSFWfilter.value) {
14787 if ((numFiltered || numNsfwHidden) && modules['filteReddit'].options.notification.value) {
14788 var notification = [];
14789 if (numFiltered) notification.push( numFiltered + ' post(s) hidden by ' + modules['settingsNavigation'].makeUrlHashLink('filteReddit', 'keywords', 'custom filters') + '.');
14790 if (numNsfwHidden) notification.push ( numNsfwHidden + ' post(s) hidden by the ' + modules['settingsNavigation'].makeUrlHashLink('filteReddit', 'NSFWfilter', 'NSFW filter') + '.');
14791 if (numNsfwHidden && modules['filteReddit'].options.NSFWQuickToggle.value) notification.push('You can toggle the nsfw filter in the <span class="gearIcon"></span> menu.');
14793 notification.push("To hide this message, disable the " + modules['settingsNavigation'].makeUrlHashLink('filteReddit', 'notification', 'filteReddit notification') + '.');
14794 var notification = notification.join('<br><br>');
14795 RESUtils.notification({
14796 header: 'Posts Filtered',
14797 moduleID: 'filteReddit',
14798 message: notification
14802 addedNSFWFilterStyle: false,
14803 addNSFWFilterStyle: function() {
14804 if (this.addedNSFWFilterStyle) return;
14805 this.addedNSFWFilterStyle = true;
14807 RESUtils.addCSS('body:not(.allowOver18) .over18 { display: none !important; }');
14808 RESUtils.addCSS('.thing.over18.allowOver18 { display: block !important; }');
14810 filterNSFW: function(filterOn) {
14811 this.addNSFWFilterStyle();
14812 $(document.body).toggleClass('allowOver18');
14814 filterTitle: function(title, reddit) {
14815 var reddit = (reddit) ? reddit.toLowerCase() : null;
14816 return this.arrayContainsSubstring(this.options.keywords.value, title.toLowerCase(), reddit);
14818 filterDomain: function(domain, reddit) {
14819 var reddit = (reddit) ? reddit.toLowerCase() : null;
14820 var domain = (domain) ? domain.toLowerCase() : null;
14821 return this.arrayContainsSubstring(this.options.domains.value, domain, reddit);
14823 filterSubreddit: function(subreddit) {
14824 return this.arrayContainsSubstring(this.options.subreddits.value, subreddit.toLowerCase(), null, true);
14826 filterFlair: function(flair, reddit) {
14827 var reddit = (reddit) ? reddit.toLowerCase() : null;
14828 return this.arrayContainsSubstring(this.options.flair.value, flair.toLowerCase(), reddit);
14830 allowAllNSFW: null, // lazy loaded with boolean-y value
14831 subredditAllowNsfwOption: null, // lazy loaded with function to get a given subreddit's row in this.options.allowNSFW
14832 allowNSFW: function (postSubreddit, currSubreddit) {
14833 if (!this.options.allowNSFW.value || !this.options.allowNSFW.value.length) return false;
14835 if (typeof currSubreddit === "undefined") {
14836 currSubreddit = RESUtils.currentSubreddit();
14839 if (!this.subredditAllowNsfwOption) {
14840 this.subredditAllowNsfwOption = RESUtils.indexOptionTable('filteReddit', 'allowNSFW', 0);
14843 if (this.allowAllNsfw == null && currSubreddit) {
14844 var optionValue = this.subredditAllowNsfwOption(currSubreddit);
14845 this.allowAllNsfw = (optionValue && optionValue[1] === 'visit') || false;
14847 if (this.allowAllNsfw) {
14851 if (!postSubreddit) postSubreddit = currSubreddit;
14852 if (!postSubreddit) return false;
14853 var optionValue = this.subredditAllowNsfwOption(postSubreddit);
14855 if (optionValue[1] === 'everywhere') {
14857 } else { // optionValue[1] == visit (subreddit or multisubreddit)
14858 if (RESUtils.inList(postSubreddit, currSubreddit, '+')) {
14864 unescapeHTML: function(theString) {
14865 var temp = document.createElement("div");
14866 $(temp).html(theString);
14867 var result = temp.childNodes[0].nodeValue;
14868 temp.removeChild(temp.firstChild);
14872 arrayContainsSubstring: function(obj, stringToSearch, reddit, fullmatch) {
14873 if (!obj) return false;
14874 stringToSearch = this.unescapeHTML(stringToSearch);
14875 var i = obj.length;
14877 if ((typeof obj[i] !== 'object') || (obj[i].length < 3)) {
14878 if (obj[i].length === 1) obj[i] = obj[i][0];
14879 obj[i] = [obj[i], 'everywhere',''];
14881 var searchString = obj[i][0];
14882 var applyTo = obj[i][1];
14883 var applyList = obj[i][2].toLowerCase().split(',');
14884 var skipCheck = false;
14885 // we also want to know if we should be matching /r/all, because when getting
14886 // listings on /r/all, each post has a subreddit (that does not equal "all")
14887 var checkRAll = ((RESUtils.currentSubreddit() === "all") && (applyList.indexOf("all") !== -1));
14890 if ((applyList.indexOf(reddit) !== -1) || (checkRAll)) {
14895 if ((applyList.indexOf(reddit) === -1) && (!checkRAll)) {
14900 // if fullmatch is defined, don't do a substring match... this is used for subreddit matching on /r/all for example
14901 if ((!skipCheck) && (fullmatch) && (obj[i] !== null) && (stringToSearch.toLowerCase() === searchString.toLowerCase())) return true;
14902 if ((!skipCheck) && (!fullmatch) && (obj[i] !== null) && (stringToSearch.indexOf(searchString.toString().toLowerCase()) !== -1)) {
14908 toggleFilter: function(e) {
14909 var thisSubreddit = $(e.target).data('subreddit').toLowerCase();
14910 var filteredReddits = modules['filteReddit'].options.subreddits.value || [];
14912 for (var i=0, len=filteredReddits.length; i<len; i++) {
14913 if ((filteredReddits[i]) && (filteredReddits[i][0].toLowerCase() === thisSubreddit)) {
14915 filteredReddits.splice(i,1);
14916 e.target.setAttribute('title','Filter this subreddit from /r/all and /domain/*');
14917 e.target.textContent = '+filter';
14918 e.target.classList.remove('remove');
14923 var thisObj = [thisSubreddit, 'everywhere',''];
14924 filteredReddits.push(thisObj);
14925 e.target.setAttribute('title','Stop filtering this subreddit from /r/all and /domain/*');
14926 e.target.textContent = '-filter';
14927 e.target.classList.add('remove');
14929 modules['filteReddit'].options.subreddits.value = filteredReddits;
14930 // save change to options...
14931 RESStorage.setItem('RESoptions.filteReddit', JSON.stringify(modules['filteReddit'].options));
14935 modules['newCommentCount'] = {
14936 moduleID: 'newCommentCount',
14937 moduleName: 'New Comment Count',
14938 category: 'Comments',
14940 // any configurable options you have go here...
14941 // options must have a type and a value..
14942 // valid types are: text, boolean (if boolean, value must be true or false)
14947 description: 'Clean out cached comment counts of pages you haven\'t visited in [x] days - enter a number here only!'
14949 subscriptionLength: {
14952 description: 'Automatically remove thread subscriptions in [x] days - enter a number here only!'
14955 description: 'Shows how many new comments there are since your last visit.',
14956 isEnabled: function() {
14957 return RESConsole.getModulePrefs(this.moduleID);
14960 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
14962 isMatchURL: function() {
14963 return RESUtils.isMatchURL(this.moduleID);
14965 beforeLoad: function() {
14966 if ((this.isEnabled()) && (this.isMatchURL())) {
14967 RESUtils.addCSS('.newComments { display: inline; color: orangered; }');
14968 RESUtils.addCSS('.RESSubscriptionButton { display: inline-block; margin-left: 15px; padding: 1px 0; text-align: center; width: 78px; font-weight: bold; cursor: pointer; color: #369; border: 1px solid #b6b6b6; border-radius: 3px; }');
14969 RESUtils.addCSS('td .RESSubscriptionButton { margin-left: 0; margin-right: 15px; } ');
14970 RESUtils.addCSS('.RESSubscriptionButton.unsubscribe { color: orangered; }');
14971 RESUtils.addCSS('.RESSubscriptionButton:hover { background-color: #f0f3fc; }');
14975 if ((this.isEnabled()) && (this.isMatchURL())) {
14977 var counts = RESStorage.getItem('RESmodules.newCommentCount.counts');
14978 if (counts == null) counts = '{}';
14979 this.commentCounts = safeJSON.parse(counts, 'RESmodules.newCommentCount.counts');
14980 if (RESUtils.pageType() === 'comments') {
14981 this.updateCommentCount();
14983 RESUtils.watchForElement('newCommentsForms', modules['newCommentCount'].updateCommentCountFromMyComment);
14984 this.addSubscribeLink();
14985 } else if (RESUtils.currentSubreddit('dashboard')) {
14986 // If we're on the dashboard, add a tab to it...
14987 // add tab to dashboard
14988 modules['dashboard'].addTab('newCommentsContents','My Subscriptions');
14989 // populate the contents of the tab
14990 var showDiv = $('<div class="show">Show:</div>')
14991 var subscriptionFilter = $('<select id="subscriptionFilter"><option>subscribed threads</option><option>all threads</option></select>')
14992 $(showDiv).append(subscriptionFilter);
14993 $('#newCommentsContents').append(showDiv);
14994 $('#subscriptionFilter').change(function(){
14995 modules['newCommentCount'].drawSubscriptionsTable();
14997 var thisTable = $('<table id="newCommentsTable" />');
14998 $(thisTable).append('<thead><tr><th sort="" class="active">Thread title</th><th sort="subreddit">Subreddit</th><th sort="updateTime">Last Visited</th><th sort="subscriptionDate">Subscription Expires</th><th class="actions">Actions</th></tr></thead><tbody></tbody>');
14999 $('#newCommentsContents').append(thisTable);
15000 $('#newCommentsTable thead th').click(function(e) {
15001 e.preventDefault();
15002 if ($(this).hasClass('actions')) {
15005 if ($(this).hasClass('active')) {
15006 $(this).toggleClass('descending');
15008 $(this).addClass('active');
15009 $(this).siblings().removeClass('active').find('SPAN').remove();
15010 $(this).find('.sortAsc, .sortDesc').remove();
15011 ($(e.target).hasClass('descending')) ? $(this).append('<span class="sortDesc" />') : $(this).append('<span class="sortAsc" />');
15012 modules['newCommentCount'].drawSubscriptionsTable($(e.target).attr('sort'), $(e.target).hasClass('descending'));
15014 this.drawSubscriptionsTable();
15015 RESUtils.watchForElement('siteTable', modules['newCommentCount'].processCommentCounts);
15017 document.body.addEventListener('DOMNodeInserted', function(event) {
15018 if ((event.target.tagName === 'DIV') && (event.target.getAttribute('id') && event.target.getAttribute('id').indexOf('siteTable') !== -1)) {
15019 modules['newCommentCount'].processCommentCounts(event.target);
15024 this.processCommentCounts();
15025 RESUtils.watchForElement('siteTable', modules['newCommentCount'].processCommentCounts);
15027 document.body.addEventListener('DOMNodeInserted', function(event) {
15028 if ((event.target.tagName === 'DIV') && (event.target.getAttribute('id') && event.target.getAttribute('id').indexOf('siteTable') !== -1)) {
15029 modules['newCommentCount'].processCommentCounts(event.target);
15034 this.checkSubscriptions();
15037 drawSubscriptionsTable: function(sortMethod, descending) {
15038 var filterType = $('#subscriptionFilter').val();
15039 this.currentSortMethod = sortMethod || this.currentSortMethod;
15040 this.descending = (descending == null) ? this.descending : descending == true;
15041 var thisCounts = [];
15042 for (var i in this.commentCounts) {
15043 this.commentCounts[i].id = i;
15044 // grab the subreddit out of the URL and store it in match[i]
15045 var match = this.commentCounts[i].url.match(RESUtils.matchRE);
15047 this.commentCounts[i].subreddit = match[1].toLowerCase();
15048 thisCounts.push(this.commentCounts[i]);
15051 $('#newCommentsTable tbody').html('');
15052 switch (this.currentSortMethod) {
15053 case 'subscriptionDate':
15054 thisCounts.sort(function(a,b) {
15055 return (a.subscriptionDate > b.subscriptionDate) ? 1 : (b.subscriptionDate > a.subscriptionDate) ? -1 : 0;
15057 if (this.descending) thisCounts.reverse();
15060 thisCounts.sort(function(a,b) {
15061 return (a.updateTime > b.updateTime) ? 1 : (b.updateTime > a.updateTime) ? -1 : 0;
15063 if (this.descending) thisCounts.reverse();
15066 thisCounts.sort(function(a,b) {
15067 return (a.subreddit > b.subreddit) ? 1 : (b.subreddit > a.subreddit) ? -1 : 0;
15069 if (this.descending) thisCounts.reverse();
15072 thisCounts.sort(function(a,b) {
15073 return (a.title > b.title) ? 1 : (b.title > a.title) ? -1 : 0;
15075 if (this.descending) thisCounts.reverse();
15079 for (var i in thisCounts) {
15080 if ((filterType === 'all threads') || ((filterType === 'subscribed threads') && (typeof thisCounts[i].subscriptionDate !== 'undefined'))) {
15081 var thisTitle = thisCounts[i].title;
15082 var thisURL = thisCounts[i].url;
15083 var thisUpdateTime = new Date(thisCounts[i].updateTime);
15084 // expire time is this.options.subscriptionLength.value days, so: 1000ms * 60s * 60m * 24hr = 86400000
15085 // then multiply by this.options.subscriptionLength.value
15086 var thisSubscriptionExpirationDate = (typeof thisCounts[i].subscriptionDate !== 'undefined') ? new Date(thisCounts[i].subscriptionDate + (86400000 * this.options.subscriptionLength.value)) : 0;
15087 if (thisSubscriptionExpirationDate > 0) {
15088 var thisExpiresContent = RESUtils.niceDateTime(thisSubscriptionExpirationDate);
15089 var thisRenewButton = '<span class="RESSubscriptionButton renew" title="renew subscription to this thread" data-threadid="'+thisCounts[i].id+'">renew</span>';
15090 var thisUnsubButton = '<span class="RESSubscriptionButton unsubscribe" title="unsubscribe from this thread" data-threadid="'+thisCounts[i].id+'">unsubscribe</span>';
15091 var thisActionContent = thisRenewButton+thisUnsubButton;
15094 var thisExpiresContent = 'n/a';
15095 var thisActionContent = '<span class="RESSubscriptionButton subscribe" title="subscribe to this thread" data-threadid="'+thisCounts[i].id+'">subscribe</span>';
15097 var thisSubreddit = '<a href="/r/'+thisCounts[i].subreddit+'">/r/'+thisCounts[i].subreddit+'</a>';
15098 var thisROW = $('<tr><td><a href="'+thisURL+'">'+thisTitle+'</a></td><td>'+thisSubreddit+'</td><td>'+RESUtils.niceDateTime(thisUpdateTime)+'</td><td>'+thisExpiresContent+'</td><td>'+thisActionContent+'</td></tr>');
15099 $(thisROW).find('.renew').click(modules['newCommentCount'].renewSubscriptionButton);
15100 $(thisROW).find('.unsubscribe').click(modules['newCommentCount'].unsubscribeButton);
15101 $(thisROW).find('.subscribe').click(modules['newCommentCount'].subscribeButton);
15102 $('#newCommentsTable tbody').append(thisROW);
15107 if (filterType === 'subscribed threads') {
15108 $('#newCommentsTable tbody').append('<td colspan="5">You are currently not subscribed to any threads. To subscribe to a thread, click the "subscribe" button found near the top of the comments page.</td>');
15110 $('#newCommentsTable tbody').append('<td colspan="5">No threads found</td>');
15114 renewSubscriptionButton: function(e) {
15115 var thisURL = $(e.target).attr('data-threadid');
15116 modules['newCommentCount'].renewSubscription(thisURL);
15117 RESUtils.notification({
15118 header: 'Subscription Notification',
15119 moduleID: 'newCommentCount',
15120 optionKey: 'subscriptionLength',
15121 message: 'Your subscription has been renewed - it will expire in '+modules['newCommentCount'].options.subscriptionLength.value+' days.'
15124 renewSubscription: function(threadid) {
15125 var now = new Date();
15126 modules['newCommentCount'].commentCounts[threadid].subscriptionDate = now.getTime();
15127 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
15128 this.drawSubscriptionsTable();
15130 unsubscribeButton: function(e) {
15131 var confirmunsub = window.confirm('Are you sure you want to unsubscribe?');
15132 if (confirmunsub) {
15133 var thisURL = $(e.target).attr('data-threadid');
15134 modules['newCommentCount'].unsubscribe(thisURL);
15137 unsubscribe: function(threadid) {
15138 delete modules['newCommentCount'].commentCounts[threadid].subscriptionDate;
15139 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
15140 this.drawSubscriptionsTable();
15142 subscribeButton: function(e) {
15143 var thisURL = $(e.target).attr('data-threadid');
15144 modules['newCommentCount'].subscribe(thisURL);
15146 subscribe: function(threadid) {
15147 var now = new Date();
15148 modules['newCommentCount'].commentCounts[threadid].subscriptionDate = now.getTime();
15149 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
15150 this.drawSubscriptionsTable();
15152 processCommentCounts: function(ele) {
15153 var ele = ele || document.body;
15154 var lastClean = RESStorage.getItem('RESmodules.newCommentCount.lastClean');
15155 var now = new Date();
15156 if (lastClean == null) {
15157 lastClean = now.getTime();
15158 RESStorage.setItem('RESmodules.newCommentCount.lastClean', now.getTime());
15160 // Clean cache every six hours
15161 if ((now.getTime() - lastClean) > 21600000) {
15162 modules['newCommentCount'].cleanCache();
15164 var IDre = /\/r\/[\w]+\/comments\/([\w]+)\//i;
15165 var commentsLinks = ele.querySelectorAll('.sitetable.linklisting div.thing.link a.comments');
15166 for (var i=0, len=commentsLinks.length; i<len;i++) {
15167 var href = commentsLinks[i].getAttribute('href');
15168 var thisCount = commentsLinks[i].innerHTML;
15169 var split = thisCount.split(' ');
15170 thisCount = split[0];
15171 var matches = IDre.exec(href);
15173 var thisID = matches[1];
15174 if ((typeof modules['newCommentCount'].commentCounts[thisID] !== 'undefined') && (modules['newCommentCount'].commentCounts[thisID] !== null)) {
15175 var diff = thisCount - modules['newCommentCount'].commentCounts[thisID].count;
15177 var newString = $('<span class="newComments"> ('+diff+' new)</span>');
15178 $(commentsLinks[i]).append(newString);
15184 updateCommentCountFromMyComment: function() {
15185 modules['newCommentCount'].updateCommentCount(true);
15187 updateCommentCount: function(mycomment) {
15188 var thisModule = modules['newCommentCount'];
15189 var IDre = /\/r\/[\w]+\/comments\/([\w]+)\//i;
15190 var matches = IDre.exec(location.href);
15192 if (!thisModule.currentCommentCount) {
15193 thisModule.currentCommentID = matches[1];
15194 var thisCount = document.querySelector('#siteTable a.comments');
15196 var split = thisCount.innerHTML.split(' ');
15197 thisModule.currentCommentCount = split[0];
15198 if ((typeof thisModule.commentCounts[thisModule.currentCommentID] !== 'undefined') && (thisModule.commentCounts[thisModule.currentCommentID] !== null)) {
15199 var prevCommentCount = thisModule.commentCounts[thisModule.currentCommentID].count;
15200 var diff = thisModule.currentCommentCount - prevCommentCount;
15201 var newString = $('<span class="newComments"> ('+diff+' new)</span>');
15202 if (diff>0) $(thisCount).append(newString);
15204 if (isNaN(thisModule.currentCommentCount)) thisModule.currentCommentCount = 0;
15205 if (mycomment) thisModule.currentCommentCount++;
15208 thisModule.currentCommentCount++;
15211 var now = new Date();
15212 if (typeof thisModule.commentCounts === 'undefined') {
15213 thisModule.commentCounts = {};
15215 if (typeof thisModule.commentCounts[thisModule.currentCommentID] === 'undefined') {
15216 thisModule.commentCounts[thisModule.currentCommentID] = {};
15218 thisModule.commentCounts[thisModule.currentCommentID].count = thisModule.currentCommentCount;
15219 thisModule.commentCounts[thisModule.currentCommentID].url = location.href.replace(location.hash, '');
15220 thisModule.commentCounts[thisModule.currentCommentID].title = document.title;
15221 thisModule.commentCounts[thisModule.currentCommentID].updateTime = now.getTime();
15222 // if (this.currentCommentCount) {
15223 // dumb, but because of Greasemonkey security restrictions we need a window.setTimeout here...
15224 window.setTimeout( function() {
15225 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
15229 cleanCache: function() {
15230 var now = new Date();
15231 for (var i in this.commentCounts) {
15232 if ((this.commentCounts[i] !== null) && ((now.getTime() - this.commentCounts[i].updateTime) > (86400000 * this.options.cleanComments.value))) {
15233 // this.commentCounts[i] = null;
15234 delete this.commentCounts[i];
15235 } else if (this.commentCounts[i] == null) {
15236 delete this.commentCounts[i];
15239 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(this.commentCounts));
15240 RESStorage.setItem('RESmodules.newCommentCount.lastClean', now.getTime());
15242 addSubscribeLink: function() {
15243 var commentCount = document.body.querySelector('.commentarea .panestack-title');
15244 if (commentCount) {
15245 this.commentSubToggle = createElementWithID('span','REScommentSubToggle','RESSubscriptionButton');
15246 this.commentSubToggle.addEventListener('click', modules['newCommentCount'].toggleSubscription, false);
15247 commentCount.appendChild(this.commentSubToggle);
15248 if (typeof this.commentCounts[this.currentCommentID].subscriptionDate !== 'undefined') {
15249 this.commentSubToggle.textContent = 'unsubscribe';
15250 this.commentSubToggle.setAttribute('title','unsubscribe from thread');
15251 this.commentSubToggle.classList.add('unsubscribe');
15253 this.commentSubToggle.textContent = 'subscribe';
15254 this.commentSubToggle.setAttribute('title','subscribe to this thread to be notified when new comments are posted');
15255 this.commentSubToggle.classList.remove('unsubscribe');
15259 toggleSubscription: function() {
15260 var commentID = modules['newCommentCount'].currentCommentID;
15261 if (typeof modules['newCommentCount'].commentCounts[commentID].subscriptionDate !== 'undefined') {
15262 modules['newCommentCount'].unsubscribeFromThread(commentID);
15264 modules['newCommentCount'].subscribeToThread(commentID);
15267 getLatestCommentCounts: function() {
15268 var counts = RESStorage.getItem('RESmodules.newCommentCount.counts');
15269 if (counts == null) {
15272 modules['newCommentCount'].commentCounts = safeJSON.parse(counts, 'RESmodules.newCommentCount.counts');
15274 subscribeToThread: function(commentID) {
15275 modules['newCommentCount'].getLatestCommentCounts();
15276 modules['newCommentCount'].commentSubToggle.textContent = 'unsubscribe';
15277 modules['newCommentCount'].commentSubToggle.setAttribute('title','unsubscribe from thread');
15278 modules['newCommentCount'].commentSubToggle.classList.add('unsubscribe');
15279 commentID = commentID || modules['newCommentCount'].currentCommentID;
15280 var now = new Date();
15281 modules['newCommentCount'].commentCounts[commentID].subscriptionDate = now.getTime();
15282 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
15283 RESUtils.notification({
15284 header: 'Subscription Notification',
15285 moduleID: 'newCommentCount',
15286 optionKey: 'subscriptionLength',
15287 message: 'You are now subscribed to this thread for '+modules['newCommentCount'].options.subscriptionLength.value+' days. You will be notified if new comments are posted since your last visit.' +
15288 '<br><br><a href="/r/Dashboard#newCommentsContents" target="_blank">Visit your Dashboard</a> to see all your thread subscriptions.'
15291 unsubscribeFromThread: function(commentID) {
15292 modules['newCommentCount'].getLatestCommentCounts();
15293 modules['newCommentCount'].commentSubToggle.textContent = 'subscribe';
15294 modules['newCommentCount'].commentSubToggle.setAttribute('title','subscribe to this thread and be notified when new comments are posted');
15295 modules['newCommentCount'].commentSubToggle.classList.remove('unsubscribe');
15296 commentID = commentID || modules['newCommentCount'].currentCommentID;
15297 var now = new Date();
15298 delete modules['newCommentCount'].commentCounts[commentID].subscriptionDate;
15299 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
15300 RESUtils.notification({
15301 header: 'Subscription Notification',
15302 moduleID: 'newCommentCount',
15303 message: 'You are now unsubscribed from this thread.'
15306 checkSubscriptions: function() {
15307 if (this.commentCounts) {
15308 var threadsToCheck = [];
15309 for (var i in this.commentCounts) {
15310 var thisSubscription = this.commentCounts[i];
15311 if ((thisSubscription) && (typeof thisSubscription.subscriptionDate !== 'undefined')) {
15312 var lastCheck = parseInt(thisSubscription.lastCheck, 10) || 0;
15313 var subscriptionDate = parseInt(thisSubscription.subscriptionDate, 10);
15314 // If it's been subscriptionLength days since we've subscribed, we're going to delete this subscription...
15315 var now = new Date();
15316 if ((now.getTime() - subscriptionDate) > (this.options.subscriptionLength.value * 86400000)) {
15317 delete this.commentCounts[i].subscriptionDate;
15319 // if we haven't checked this subscription in 5 minutes, try it again...
15320 if ((now.getTime() - lastCheck) > 300000) {
15321 thisSubscription.lastCheck = now.getTime();
15322 this.commentCounts[i] = thisSubscription;
15323 // this.checkThread(i);
15324 threadsToCheck.push('t3_'+i);
15326 RESStorage.setItem('RESmodules.newCommentCount.count', JSON.stringify(this.commentCounts));
15329 if (threadsToCheck.length > 0) {
15330 this.checkThreads(threadsToCheck);
15334 checkThreads: function(commentIDs) {
15335 GM_xmlhttpRequest({
15337 url: location.protocol + '//' + location.hostname + '/by_id/' + commentIDs.join(',') + '.json?app=res',
15338 onload: function(response) {
15339 var now = new Date();
15340 var commentInfo = JSON.parse(response.responseText);
15341 if (typeof commentInfo.data !== 'undefined') {
15342 for (var i=0, len=commentInfo.data.children.length; i<len; i++) {
15343 var commentID = commentInfo.data.children[i].data.id;
15344 var subObj = modules['newCommentCount'].commentCounts[commentID];
15345 if (subObj.count < commentInfo.data.children[i].data.num_comments) {
15346 modules['newCommentCount'].commentCounts[commentID].count = commentInfo.data.children[i].data.num_comments;
15347 RESUtils.notification({
15348 header: 'Subscription Notification',
15349 moduleID: 'newComments',
15350 message: '<p>New comments posted to thread:</p> <a href="'+subObj.url+'">' + subObj.title + '</a> <p><a class="RESNotificationButtonBlue" href="'+subObj.url+'">view the submission</a></p><div class="clear"></div>'
15354 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
15361 modules['spamButton'] = {
15362 moduleID: 'spamButton',
15363 moduleName: 'Spam Button',
15364 category: 'Filters',
15367 description: 'Adds a Spam button to posts for easy reporting.',
15368 isEnabled: function() {
15369 return RESConsole.getModulePrefs(this.moduleID);
15372 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
15374 isMatchURL: function() {
15375 return RESUtils.isMatchURL(this.moduleID);
15378 if ((this.isEnabled()) && (this.isMatchURL())) {
15379 // check if the spam button was on by default from an old install of RES. Per Reddit Admin request, this is being
15380 // disabled by default due to excess misuse, but people who want to purposefully re-enable it may do so.
15381 var reset = RESStorage.getItem('RESmodules.spamButton.reset');
15383 RESStorage.setItem('RESmodules.spamButton.reset','true');
15384 RESConsole.enableModule('spamButton', false);
15387 // credit to tico24 for the idea, here: http://userscripts.org/scripts/review/84454
15388 // code adapted for efficiency...
15389 if (RESUtils.loggedInUser() !== RESUtils.currentUserProfile()) {
15390 RESUtils.watchForElement('siteTable', modules['spamButton'].addSpamButtons);
15391 this.addSpamButtons();
15395 addSpamButtons: function(ele) {
15396 if (ele == null) ele = document;
15397 if ((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments') || (RESUtils.pageType() === 'profile')) {
15398 var allLists = ele.querySelectorAll('#siteTable ul.flat-list.buttons');
15399 for(var i=0, len=allLists.length; i<len; i++)
15401 var permaLink = allLists[i].childNodes[0].childNodes[0].href;
15403 var spam = document.createElement('li');
15404 // insert spam button second to last in the list... this is a bit hacky and assumes singleClick is enabled...
15405 // it should probably be made smarter later, but there are so many variations of configs, etc, that it's a bit tricky.
15406 allLists[i].lastChild.parentNode.insertBefore(spam, allLists[i].lastChild);
15408 // it's faster to figure out the author only if someone actually clicks the link, so we're modifying the code to listen for clicks and not do all that queryselector stuff.
15409 var a = document.createElement('a');
15410 a.setAttribute('class', 'option');
15411 a.setAttribute('title', 'Report this user as a spammer');
15412 a.addEventListener('click', modules['spamButton'].reportPost, false);
15413 a.setAttribute('href', 'javascript:void(0)');
15414 a.textContent= 'rts';
15415 a.title = "reportthespammers"
15416 spam.appendChild(a);
15420 reportPost: function(e) {
15422 var authorProfileContainer = a.parentNode.parentNode.parentNode;
15423 var authorProfileLink = authorProfileContainer.querySelector('.author');
15424 var href = authorProfileLink.href;
15425 var authorName = authorProfileLink.innerHTML;
15426 a.setAttribute('href', 'http://www.reddit.com/r/reportthespammers/submit?url=' + href + '&title=overview for '+authorName);
15427 a.setAttribute('target', '_blank');
15431 modules['commentNavigator'] = {
15432 moduleID: 'commentNavigator',
15433 moduleName: 'Comment Navigator',
15434 category: 'Comments',
15435 description: 'Provides a comment navigation tool to easily find comments by OP, mod, etc.',
15440 description: 'Display Comment Navigator by default'
15443 isEnabled: function() {
15444 return RESConsole.getModulePrefs(this.moduleID);
15447 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
15448 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
15450 isMatchURL: function() {
15451 return RESUtils.isMatchURL(this.moduleID);
15453 beforeLoad: function() {
15454 if ((this.isEnabled()) && (this.isMatchURL())) {
15455 RESUtils.addCSS('#REScommentNavBox { position: fixed; z-index: 999; right: 10px; top: 46px; width: 265px; border: 1px solid gray; background-color: #fff; opacity: 0.3; padding: 3px; user-select: none; -webkit-user-select: none; -moz-user-select: none; -webkit-transition:opacity 0.5s ease-in; -moz-transition:opacity 0.5s ease-in; -o-transition:opacity 0.5s ease-in; -ms-transition:opacity 0.5s ease-in; -transition:opacity 0.5s ease-in; }');
15456 RESUtils.addCSS('#REScommentNavBox:hover { opacity: 1 }');
15457 RESUtils.addCSS('#REScommentNavToggle { float: left; display: inline; margin-left: 0; width: 100%; }');
15458 RESUtils.addCSS('.commentarea .menuarea { margin-right: 0; }');
15459 RESUtils.addCSS('.menuarea > .spacer { margin-right: 0; }');
15460 RESUtils.addCSS('#commentNavButtons { margin: auto; }');
15461 RESUtils.addCSS('#commentNavUp { margin: auto; cursor: pointer; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); width: 32px; height: 20px; background-position: 0 -224px; }');
15462 RESUtils.addCSS('#commentNavDown { margin: auto; cursor: pointer; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); width: 32px; height: 20px; background-position: 0 -244px; }');
15463 RESUtils.addCSS('#commentNavUp.noNav { background-position: 0 -264px; }');
15464 RESUtils.addCSS('#commentNavDown.noNav { background-position: 0 -284px; }');
15465 RESUtils.addCSS('#commentNavButtons { display: none; margin-left: 12px; text-align: center; user-select: none; -webkit-user-select: none; -moz-user-select: none; }');
15466 RESUtils.addCSS('.commentNavSortType { cursor: pointer; font-weight: bold; float: left; margin-left: 6px; }');
15467 RESUtils.addCSS('#commentNavPostCount { color: #1278d3; }');
15468 RESUtils.addCSS('.noNav #commentNavPostCount { color: #ddd; }');
15469 RESUtils.addCSS('.commentNavSortTypeDisabled { color: #ddd; }');
15470 RESUtils.addCSS('.commentNavSortType:hover { text-decoration: underline; }');
15471 RESUtils.addCSS('#REScommentNavToggle span { float: left; margin-left: 6px; }');
15472 RESUtils.addCSS('.menuarea > .spacer { float: left; }');
15476 if ((this.isEnabled()) && (this.isMatchURL())) {
15477 // draw the commentNav box
15478 this.commentNavBox = createElementWithID('div','REScommentNavBox');
15479 this.commentNavBox.classList.add('RESDialogSmall');
15480 // var commentArea = document.body.querySelector('div.sitetable.nestedlisting');
15481 var commentArea = document.body.querySelector('.commentarea .menuarea');
15483 this.commentNavToggle = createElementWithID('div','REScommentNavToggle');
15484 $(this.commentNavToggle).html('<span>navigate by:</span>');
15485 var sortTypes = ['submitter', 'moderator', 'friend', 'me', 'admin', 'IAmA', 'images', 'popular', 'new'];
15486 for (var i=0, len=sortTypes.length; i<len; i++) {
15487 var thisCategory = sortTypes[i];
15488 // var thisEle = document.createElement('div');
15489 var thisEle = createElementWithID('div','navigateBy'+thisCategory);
15490 switch(thisCategory) {
15492 thisEle.setAttribute('title','Navigate comments made by the post submitter');
15495 thisEle.setAttribute('title','Navigate comments made by moderators');
15498 thisEle.setAttribute('title','Navigate comments made by users on your friends list');
15501 thisEle.setAttribute('title','Navigate comments made by you');
15504 thisEle.setAttribute('title','Navigate comments made by reddit admins');
15507 thisEle.setAttribute('title','Navigate through questions that have been answered by the submitter (most useful in /r/IAmA)');
15510 thisEle.setAttribute('title','Navigate through comments with images');
15513 thisEle.setAttribute('title','Navigate through comments in order of highest vote total');
15516 thisEle.setAttribute('title','Navigate through new comments (Reddit Gold users only)');
15521 thisEle.setAttribute('index',i+1);
15522 thisEle.classList.add('commentNavSortType');
15523 thisEle.textContent = thisCategory;
15524 if (thisCategory === 'new') {
15525 var isGold = document.body.querySelector('.gold-accent.comment-visits-box');
15527 thisEle.setAttribute('style','color: #9A7D2E;');
15529 thisEle.classList.add('commentNavSortTypeDisabled');
15532 if ((thisCategory !== 'new') || (isGold)) {
15533 thisEle.addEventListener('click', function(e) {
15534 modules['commentNavigator'].showNavigator(e.target.getAttribute('index'));
15537 this.commentNavToggle.appendChild(thisEle);
15539 var thisDivider = document.createElement('span');
15540 thisDivider.textContent = '|';
15541 this.commentNavToggle.appendChild(thisDivider);
15545 // commentArea.insertBefore(this.commentNavToggle,commentArea.firstChild);
15546 commentArea.appendChild(this.commentNavToggle,commentArea.firstChild);
15547 if (!(this.options.showByDefault.value)) {
15548 this.commentNavBox.style.display = 'none';
15550 var navBoxHTML = ' \
15553 <select id="commentNavBy"> \
15554 <option name=""></option> \
15555 <option name="submitter">submitter</option> \
15556 <option name="moderator">moderator</option> \
15557 <option name="friend">friend</option> \
15558 <option name="me">me</option> \
15559 <option name="admin">admin</option> \
15560 <option name="IAmA">IAmA</option> \
15561 <option name="images">images</option> \
15562 <option name="popular">popular</option> \
15563 <option name="new">new</option> \
15566 <div id="commentNavCloseButton" class="RESCloseButton">×</div> \
15567 <div class="RESDialogContents"> \
15568 <div id="commentNavButtons"> \
15569 <div id="commentNavUp"></div> <div id="commentNavPostCount"></div> <div id="commentNavDown"></div> \
15573 $(this.commentNavBox).html(navBoxHTML);
15576 this.navSelect = this.commentNavBox.querySelector('#commentNavBy');
15577 this.commentNavPostCount = this.commentNavBox.querySelector('#commentNavPostCount');
15578 this.commentNavButtons = this.commentNavBox.querySelector('#commentNavButtons');
15579 this.commentNavCloseButton = this.commentNavBox.querySelector('#commentNavCloseButton');
15580 this.commentNavCloseButton.addEventListener('click', function(e) {
15581 modules['commentNavigator'].commentNavBox.style.display = 'none';
15583 this.commentNavUp = this.commentNavBox.querySelector('#commentNavUp');
15584 this.commentNavUp.addEventListener('click',modules['commentNavigator'].moveUp, false);
15585 this.commentNavDown = this.commentNavBox.querySelector('#commentNavDown');
15586 this.commentNavDown.addEventListener('click',modules['commentNavigator'].moveDown, false);
15587 this.navSelect.addEventListener('change', modules['commentNavigator'].changeCategory, false);
15588 document.body.appendChild(this.commentNavBox);
15592 changeCategory: function() {
15593 var index = modules['commentNavigator'].navSelect.selectedIndex;
15594 modules['commentNavigator'].currentCategory = modules['commentNavigator'].navSelect.options[index].value;
15595 if (modules['commentNavigator'].currentCategory !== '') {
15596 modules['commentNavigator'].getPostsByCategory(modules['commentNavigator'].currentCategory);
15597 modules['commentNavigator'].commentNavButtons.style.display = 'block';
15599 modules['commentNavigator'].commentNavButtons.style.display = 'none';
15602 showNavigator: function(categoryID) {
15603 modules['commentNavigator'].commentNavBox.style.display = 'block';
15604 this.navSelect.selectedIndex = categoryID;
15605 modules['commentNavigator'].changeCategory();
15607 getPostsByCategory: function () {
15608 var category = modules['commentNavigator'].currentCategory;
15609 if ((typeof category !== 'undefined') && (category !== '')) {
15610 if (typeof this.posts[category] === 'undefined') {
15611 switch (category) {
15616 this.posts[category] = document.querySelectorAll('.noncollapsed a.author.'+category);
15617 this.resetNavigator(category);
15620 RESUtils.getUserInfo(function(userInfo) {
15621 var myID = 't2_'+userInfo.data.id;
15622 modules['commentNavigator'].posts[category] = document.querySelectorAll('.noncollapsed a.author.id-'+myID);
15623 modules['commentNavigator'].resetNavigator(category);
15627 var submitterPosts = document.querySelectorAll('.noncollapsed a.author.submitter');
15628 this.posts[category] = [];
15629 for (var i=0, len=submitterPosts.length; i<len; i++) {
15630 // go seven parents up to get the proper parent post...
15631 var sevenUp = submitterPosts[i].parentNode.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
15632 if (sevenUp.parentNode.nodeName === 'BODY') {
15633 this.posts[category].push(submitterPosts[i].parentNode.parentNode);
15635 this.posts[category].push(sevenUp);
15638 this.resetNavigator(category);
15641 var imagePosts = document.querySelectorAll('.toggleImage');
15642 this.posts[category] = imagePosts;
15643 this.resetNavigator(category);
15646 var allComments = document.querySelectorAll('.noncollapsed');
15647 var commentsObj = [];
15648 for (var i=0, len=allComments.length; i<len; i++) {
15649 var thisScore = allComments[i].querySelector('.unvoted');
15651 var scoreSplit = thisScore.innerHTML.split(' ');
15652 var score = scoreSplit[0];
15657 comment: allComments[i],
15661 commentsObj.sort(function(a, b) {
15662 return parseInt(b.score, 10) - parseInt(a.score, 10);
15664 this.posts[category] = [];
15665 for (var i=0, len=commentsObj.length; i<len; i++) {
15666 this.posts[category][i] = commentsObj[i].comment;
15668 this.resetNavigator(category);
15671 this.posts[category] = document.querySelectorAll('.new-comment');
15672 this.resetNavigator(category);
15678 resetNavigator: function(category) {
15679 this.nav[category] = 0;
15680 if (this.posts[category].length) {
15681 modules['commentNavigator'].scrollToNavElement();
15682 modules['commentNavigator'].commentNavUp.classList.remove('noNav');
15683 modules['commentNavigator'].commentNavDown.classList.remove('noNav');
15684 modules['commentNavigator'].commentNavButtons.classList.remove('noNav');
15686 modules['commentNavigator'].commentNavPostCount.textContent = 'none';
15687 modules['commentNavigator'].commentNavUp.classList.add('noNav');
15688 modules['commentNavigator'].commentNavDown.classList.add('noNav');
15689 modules['commentNavigator'].commentNavButtons.classList.add('noNav');
15692 moveUp: function() {
15693 var category = modules['commentNavigator'].currentCategory;
15694 if (modules['commentNavigator'].posts[category].length) {
15695 if (modules['commentNavigator'].nav[category] > 0) {
15696 modules['commentNavigator'].nav[category]--;
15698 modules['commentNavigator'].nav[category] = modules['commentNavigator'].posts[category].length - 1;
15700 modules['commentNavigator'].scrollToNavElement();
15703 moveDown: function() {
15704 var category = modules['commentNavigator'].currentCategory;
15705 if (modules['commentNavigator'].posts[category].length) {
15706 if (modules['commentNavigator'].nav[category] < modules['commentNavigator'].posts[category].length - 1) {
15707 modules['commentNavigator'].nav[category]++;
15709 modules['commentNavigator'].nav[category] = 0;
15711 modules['commentNavigator'].scrollToNavElement();
15714 scrollToNavElement: function() {
15715 var category = modules['commentNavigator'].currentCategory;
15716 $(modules['commentNavigator'].commentNavPostCount).text(modules['commentNavigator'].nav[category]+1 + '/' + modules['commentNavigator'].posts[category].length);
15717 var thisXY=RESUtils.getXYpos(modules['commentNavigator'].posts[category][modules['commentNavigator'].nav[category]]);
15718 RESUtils.scrollTo(0,thisXY.y);
15724 modules['redditProfiles'] = {
15725 moduleID: 'redditProfiles',
15726 moduleName: 'Reddit Profiles',
15730 description: 'Pulls in profiles from redditgifts.com when viewing a user profile.',
15731 isEnabled: function() {
15732 return RESConsole.getModulePrefs(this.moduleID);
15735 /^http:\/\/([a-z]+).reddit.com\/user\/[-\w\.]+/i
15737 isMatchURL: function() {
15738 return RESUtils.isMatchURL(this.moduleID);
15741 if ((this.isEnabled()) && (this.isMatchURL())) {
15742 RESUtils.addCSS('.redditGiftsProfileField { margin-top: 3px; margin-bottom: 6px; }');
15743 RESUtils.addCSS('.redditGiftsTrophy { margin-right: 4px; }');
15744 var thisCache = RESStorage.getItem('RESmodules.redditProfiles.cache');
15745 if (thisCache == null) {
15748 this.profileCache = safeJSON.parse(thisCache);
15749 if (this.profileCache == null) this.profileCache = {};
15750 var userRE = /\/user\/(\w+)/i;
15751 var match = userRE.exec(location.href);
15753 var username = match[1];
15754 this.getProfile(username);
15758 getProfile: function(username) {
15760 if ((typeof this.profileCache[username] !== 'undefined') && (this.profileCache[username] !== null)) {
15761 lastCheck = this.profileCache[username].lastCheck;
15763 var now = new Date();
15764 if ((now.getTime() - lastCheck) > 900000) {
15765 var jsonURL = 'http://redditgifts.com/profiles/view-json/'+username+'/';
15766 GM_xmlhttpRequest({
15769 onload: function(response) {
15771 // if it is JSON parseable, it's a profile.
15772 var profileData = JSON.parse(response.responseText);
15774 // if it is NOT JSON parseable, it's a 404 - user doesn't have a profile.
15775 var profileData = {};
15777 var now = new Date();
15778 profileData.lastCheck = now.getTime();
15779 // set the last check time...
15780 modules['redditProfiles'].profileCache[username] = profileData;
15781 RESStorage.setItem('RESmodules.redditProfiles.cache', JSON.stringify(modules['redditProfiles'].profileCache));
15782 modules['redditProfiles'].displayProfile(username, profileData);
15786 this.displayProfile(username, this.profileCache[username]);
15789 displayProfile: function(username, profileObject) {
15790 if (typeof profileObject !== 'undefined') {
15791 var firstSpacer = document.querySelector('div.side > div.spacer');
15792 var newSpacer = document.createElement('div');
15793 var profileHTML = '<div class="sidecontentbox profile-area"><a class="helplink" target="_blank" href="http://redditgifts.com">what\'s this?</a><h1>PROFILE</h1><div class="content">';
15794 var profileBody = '';
15795 if (typeof profileObject.body !== 'undefined') {
15796 profileBody += '<h3><a target="_blank" href="http://redditgifts.com/profiles/view/'+username+'">RedditGifts Profile:</a></h3>';
15797 profileBody += '<div class="redditGiftsProfileField">'+profileObject.body+'</div>';
15799 if (typeof profileObject.description !== 'undefined') {
15800 profileBody += '<h3>Description:</h3>';
15801 profileBody += '<div class="redditGiftsProfileField">'+profileObject.description+'</div>';
15803 if (typeof profileObject.photo !== 'undefined') {
15804 profileBody += '<h3>Photo:</h3>';
15805 profileBody += '<div class="redditGiftsProfileField"><a target="_blank" href="'+profileObject.photo.url+'"><img src="'+profileObject.photo_small.url+'" /></a></div>';
15807 if (typeof profileObject.twitter_username !== 'undefined') {
15808 profileBody += '<h3>Twitter:</h3>';
15809 profileBody += '<div class="redditGiftsProfileField"><a target="_blank" href="http://twitter.com/'+profileObject.twitter_username+'">@'+profileObject.twitter_username+'</a></div>';
15811 if (typeof profileObject.website !== 'undefined') {
15812 profileBody += '<h3>Website:</h3>';
15813 profileBody += '<div class="redditGiftsProfileField"><a target="_blank" href="'+profileObject.website+'">[link]</a></div>';
15815 if (typeof profileObject.trophies !== 'undefined') {
15816 profileBody += '<h3>RedditGifts Trophies:</h3>';
15818 var len=profileObject.trophies.length;
15819 for (var i in profileObject.trophies) {
15820 var rowNum = parseInt(count/2);
15822 profileBody += '<table class="trophy-table"><tbody>';
15824 // console.log('count: ' + count + ' -- mod: ' + (count%2) + ' len: ' + len);
15825 // console.log('countmod: ' + ((count%2) === 0));
15826 if ((count%2) === 1) {
15827 profileBody += '<tr>';
15829 if ((count===len) && ((count%2) === 1)) {
15830 profileBody += '<td class="trophy-info" colspan="2">';
15832 profileBody += '<td class="trophy-info">';
15834 profileBody += '<div><img src="'+profileObject.trophies[i].url+'" alt="'+profileObject.trophies[i].title+'" title="'+profileObject.trophies[i].title+'"><br><span class="trophy-name">'+profileObject.trophies[i].title+'</span></div>';
15835 profileBody += '</td>';
15836 if (((count%2) === 0) || (count===len)) {
15837 profileBody += '</tr>';
15842 profileBody += '</tbody></table>';
15845 if (profileBody === '') {
15846 profileBody = 'User has not filled out a profile on <a target="_blank" href="http://redditgifts.com">RedditGifts</a>.';
15848 profileHTML += profileBody + '</div></div>';
15849 $(newSpacer).html(profileHTML);
15850 addClass(newSpacer,'spacer');
15851 insertAfter(firstSpacer,newSpacer);
15857 modules['subredditManager'] = {
15858 moduleID: 'subredditManager',
15859 moduleName: 'Subreddit Manager',
15862 subredditShortcut: {
15865 description: 'Add +shortcut button in subreddit sidebar for easy addition of shortcuts.'
15870 description: 'Show "DASHBOARD" link in subreddit manager'
15875 description: 'Show "ALL" link in subreddit manager'
15880 description: 'show "FRONT" link in subreddit manager'
15885 description: 'Show "RANDOM" link in subreddit manager'
15890 description: 'Show "MYRANDOM" link in subreddit manager (reddit gold only)'
15895 description: 'Show "RANDNSFW" link in subreddit manager'
15900 description: 'Show "FRIENDS" link in subreddit manager'
15905 description: 'Show "MOD" link in subreddit manager'
15910 description: 'Show "MODQUEUE" link in subreddit manager'
15915 { name: 'Subreddit Name', value: 'displayName' },
15916 { name: 'Added date', value: 'addedDate' }
15918 value : 'displayName',
15919 description: 'Field to sort subreddit shortcuts by'
15921 sortingDirection: {
15924 { name: 'Ascending', value: 'asc' },
15925 { name: 'Descending', value: 'desc' }
15928 description: 'Field to sort subreddit shortcuts by'
15931 description: 'Allows you to customize the top bar with your own subreddit shortcuts, including dropdown menus of multi-reddits and more.',
15932 isEnabled: function() {
15933 return RESConsole.getModulePrefs(this.moduleID);
15936 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
15938 isMatchURL: function() {
15939 return RESUtils.isMatchURL(this.moduleID);
15941 beforeLoad: function() {
15942 if ((this.isEnabled()) && (this.isMatchURL())) {
15943 RESUtils.addCSS('.srOver { outline: 1px dashed black; }');
15944 RESUtils.addCSS('body { overflow-x: hidden; }');
15945 RESUtils.addCSS('#sr-header-area a { font-size: 100% !important; }');
15946 RESUtils.addCSS('#srList { position: absolute; top: 18px; left: 0; z-index: 9999; display: none; border: 1px solid black; background-color: #FAFAFA; width: auto; overflow-y: auto; }');
15947 RESUtils.addCSS('#srList tr { border-bottom: 1px solid gray; }');
15948 RESUtils.addCSS('#srList thead td { cursor: pointer; }');
15949 RESUtils.addCSS('#srList td { padding: 3px 8px; }');
15950 RESUtils.addCSS('#srList td.RESvisited, #srList td.RESshortcut { text-transform: none; }');
15951 RESUtils.addCSS('#srList td.RESshortcut {cursor: pointer;}');
15952 RESUtils.addCSS('#srList td a { width: 100%; display: block; }');
15953 RESUtils.addCSS('#srList tr:hover { background-color: #eef; }');
15954 RESUtils.addCSS('#srLeftContainer, #RESStaticShortcuts, #RESShortcuts, #srDropdown { display: inline; float: left; position: relative; z-index: 5; }');
15955 RESUtils.addCSS('#editShortcutDialog { display: none; z-index: 999; position: absolute; top: 25px; left: 5px; width: 230px; padding: 10px; background-color: #f0f3fc; border: 1px solid #c7c7c7; border-radius: 3px; font-size: 12px; color: #000; }');
15956 RESUtils.addCSS('#editShortcutDialog h3 { display: inline-block; float: left; font-size: 13px; margin-top: 6px; }');
15957 RESUtils.addCSS('#editShortcutClose { float: right; margin-top: 2px; margin-right: 0; }');
15958 RESUtils.addCSS('#editShortcutDialog label { clear: both; float: left; width: 100px; margin-top: 12px; }');
15959 RESUtils.addCSS('#editShortcutDialog input { float: left; width: 126px; margin-top: 10px; }');
15960 RESUtils.addCSS('#editShortcutDialog input[type=button] { float: right; width: 45px; margin-left: 10px; cursor: pointer; padding: 3px 5px; font-size: 12px; color: #fff; border: 1px solid #636363; border-radius: 3px; background-color: #5cc410; }'); RESUtils.addCSS( '#srLeftContainer { z-index: 4; padding-left: 4px; margin-right: 6px; }' ); // RESUtils.addCSS('#RESShortcuts { position: absolute; left: '+ this.srLeftContainerWidth+'px; z-index: 6; white-space: nowrap; overflow-x: hidden; padding-left: 2px; margin-top: -2px; padding-top: 2px; }');
15961 RESUtils.addCSS( '#srLeftContainer { z-index: 4; padding-left: 4px; margin-right: 6px; }' );
15962 RESUtils.addCSS('#RESShortcutsViewport { width: auto; max-height: 20px; overflow: hidden; } ');
15963 RESUtils.addCSS('#RESShortcuts { z-index: 6; white-space: nowrap; overflow-x: hidden; padding-left: 2px; }');
15964 RESUtils.addCSS('#RESSubredditGroupDropdown { display: none; position: absolute; z-index: 99999; padding: 3px; background-color: #F0F0F0; border-left: 1px solid black; border-right: 1px solid black; border-bottom: 1px solid black; }');
15965 RESUtils.addCSS('#RESSubredditGroupDropdown li { padding-left: 3px; padding-right: 3px; margin-bottom: 2px; }');
15966 RESUtils.addCSS('#RESSubredditGroupDropdown li:hover { background-color: #F0F0FC; }');
15968 RESUtils.addCSS('#RESShortcutsEditContainer { width: 69px; position: absolute; right: 0; top: 0; z-index: 999; background-color: #f0f0f0; height: 16px; user-select: none; -webkit-user-select: none; -moz-user-select: none; }');
15969 RESUtils.addCSS('#RESShortcutsRight { right: 0; }');
15970 RESUtils.addCSS('#RESShortcutsAdd { right: 15px; }');
15971 RESUtils.addCSS('#RESShortcutsLeft { right: 31px; }');
15972 RESUtils.addCSS('#RESShortcutsSort { right: 47px; }');
15974 RESUtils.addCSS('#RESShortcutsSort, #RESShortcutsRight, #RESShortcutsLeft, #RESShortcutsAdd, #RESShortcutsTrash { width: 16px; cursor: pointer; background: #F0F0F0; font-size: 20px; color: #369; height: 18px; line-height: 15px; position: absolute; top: 0; z-index: 999; background-color: #f0f0f0; user-select: none; -webkit-user-select: none; -moz-user-select: none; } ');
15975 RESUtils.addCSS('#RESShortcutsSort { font-size: 14px; }')
15976 RESUtils.addCSS('#RESShortcutsTrash { display: none; font-size: 17px; width: 16px; cursor: pointer; right: 15px; height: 16px; position: absolute; top: 0; z-index: 1000; user-select: none; -webkit-user-select: none; -moz-user-select: none; }');
15977 RESUtils.addCSS('.srSep { margin-left: 6px; }');
15978 RESUtils.addCSS('.RESshortcutside { margin-right: 5px; margin-top: 2px; color: white; background-image: url(http://www.redditstatic.com/bg-button-add.png); cursor: pointer; text-align: center; width: 68px; font-weight: bold; font-size: 10px; border: 1px solid #444; padding: 1px 6px; border-radius: 3px 3px 3px 3px; }');
15979 RESUtils.addCSS('.RESshortcutside.remove { background-image: url(http://www.redditstatic.com/bg-button-remove.png) }');
15980 RESUtils.addCSS('.RESshortcutside:hover { background-color: #f0f0ff; }');
15981 // RESUtils.addCSS('h1.redditname > a { float: left; }');
15982 RESUtils.addCSS('h1.redditname { overflow: auto; }');
15983 RESUtils.addCSS('.sortAsc, .sortDesc { float: right; background-image: url("http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png"); width: 12px; height: 12px; background-repeat: no-repeat; }');
15984 RESUtils.addCSS('.sortAsc { background-position: 0 -149px; }');
15985 RESUtils.addCSS('.sortDesc { background-position: -12px -149px; }');
15986 RESUtils.addCSS('#RESShortcutsAddFormContainer { display: none; position: absolute; width: 290px; padding: 2px; right: 0; top: 21px; z-index: 10000; background-color: #f0f3fc; border: 1px solid #c7c7c7; border-radius: 3px; font-size: 12px; color: #000; }');
15987 RESUtils.addCSS('#RESShortcutsAddFormContainer a { font-weight: bold; }');
15988 RESUtils.addCSS('#newShortcut { width: 130px; }');
15989 RESUtils.addCSS('#displayName { width: 130px; }');
15990 RESUtils.addCSS('#shortCutsAddForm { padding: 5px; }');
15991 RESUtils.addCSS('#shortCutsAddForm div { font-size: 10px; margin-bottom: 10px; }');
15992 RESUtils.addCSS('#shortCutsAddForm label { display: inline-block; width: 100px; }');
15993 RESUtils.addCSS('#shortCutsAddForm input[type=text] { width: 170px; margin-bottom: 6px; }');
15994 RESUtils.addCSS('#addSubreddit { float: right; cursor: pointer; padding: 3px 5px; font-size: 12px; color: #fff; border: 1px solid #636363; border-radius: 3px; background-color: #5cc410; }');
15995 RESUtils.addCSS('.RESShortcutsCurrentSub { color:orangered!important; font-weight:bold; }');
15996 RESUtils.addCSS('.RESShortcutsCurrentSub:visited { color:orangered!important; font-weight:bold; }');
15997 RESUtils.addCSS('#srLeftContainer, #RESShortcutsViewport, #RESShortcutsEditContainer{max-height:18px;}');
15999 // this shows the sr-header-area that we hid while rendering it (to curb opera's glitchy "jumping")...
16000 if (BrowserDetect.isOpera()) {
16001 RESUtils.addCSS('#sr-header-area { display: block !important; }');
16006 if ((this.isEnabled()) && (this.isMatchURL())) {
16008 if (this.options.linkMyRandom.value) {
16009 var originalMyRandom = document.querySelector('#sr-header-area a[href$="/r/myrandom/"]')
16010 if (originalMyRandom) {
16011 this.myRandomEnabled = true;
16012 if (originalMyRandom.classList.contains('gold')) {
16013 this.myRandomGold = true;
16018 this.manageSubreddits();
16019 if (RESUtils.currentSubreddit() !== null) {
16020 this.setLastViewtime();
16024 manageSubreddits: function() {
16025 // This is the init function for Manage Subreddits - it'll get your preferences and redraw the top bar.
16026 this.redrawSubredditBar();
16027 // Listen for subscriptions / unsubscriptions from reddits so we know to reload the JSON string...
16028 // also, add a +/- shortcut button...
16029 if ((RESUtils.currentSubreddit()) && (this.options.subredditShortcut.value == true)) {
16030 var subButtons = document.querySelectorAll('.fancy-toggle-button');
16031 // for (var h=0, len=currentSubreddits.length; h<len; h++) {
16032 for (var h=0, len=subButtons.length; h<len; h++) {
16033 var subButton = subButtons[h];
16034 if ((RESUtils.currentSubreddit().indexOf('+') === -1) && (RESUtils.currentSubreddit() !== 'mod')) {
16035 var thisSubredditFragment = RESUtils.currentSubreddit();
16036 var isMulti = false;
16037 } else if ($(subButton).parent().hasClass('subButtons')) {
16038 var isMulti = true;
16039 var thisSubredditFragment = $(subButton).parent().parent().find('a.title').text();
16041 var isMulti = true;
16042 var thisSubredditFragment = $(subButton).next().text();
16044 if ($('#subButtons-'+thisSubredditFragment).length === 0) {
16045 var subButtonsWrapper = $('<div id="subButtons-'+thisSubredditFragment+'" class="subButtons" style="margin: 0 !important;"></div>');
16046 $(subButton).wrap(subButtonsWrapper);
16047 // move this wrapper to the end (after any icons that may exist...)
16049 var theWrap = $(subButton).parent();
16050 $(theWrap).appendTo($(theWrap).parent());
16053 subButton.addEventListener('click', function() {
16054 // reset the last checked time for the subreddit list so that we refresh it anew no matter what.
16055 RESStorage.setItem('RESmodules.subredditManager.subreddits.lastCheck.'+RESUtils.loggedInUser(),0);
16057 var theSC = document.createElement('span');
16058 theSC.setAttribute('style','display: inline-block !important;');
16059 theSC.setAttribute('class','RESshortcut RESshortcutside');
16060 theSC.setAttribute('data-subreddit',thisSubredditFragment);
16062 for (var i=0, sublen=modules['subredditManager'].mySubredditShortcuts.length; i<sublen; i++) {
16063 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === thisSubredditFragment.toLowerCase()) {
16069 theSC.textContent = '-shortcut';
16070 theSC.setAttribute('title','Remove this subreddit from your shortcut bar');
16071 theSC.classList.add('remove');
16073 theSC.textContent = '+shortcut';
16074 theSC.setAttribute('title','Add this subreddit to your shortcut bar');
16076 theSC.addEventListener('click', modules['subredditManager'].toggleSubredditShortcut, false);
16077 // subButton.parentNode.insertBefore(theSC, subButton);
16078 // theSubredditLink.appendChild(theSC);
16079 $('#subButtons-'+thisSubredditFragment).append(theSC);
16080 var next = $('#subButtons-'+thisSubredditFragment).next();
16081 if ($(next).hasClass('title') && (! $('#subButtons-'+thisSubredditFragment).hasClass('swapped'))) {
16082 $('#subButtons-'+thisSubredditFragment).before($(next));
16083 $('#subButtons-'+thisSubredditFragment).addClass('swapped');
16088 // If we're on the reddit-browsing page (/reddits or /subreddits), add +shortcut and -shortcut buttons...
16089 if (/^https?:\/\/www\.reddit\.com\/(?:sub)?reddits\/?(?:\?[\w=&]+)*/.test(location.href)) {
16090 this.browsingReddits();
16093 browsingReddits: function() {
16094 $('.subreddit').each(function () {
16095 // Skip subreddit links that already have a shortcut button
16096 if (typeof $(this).data('hasShortcutButton') !== 'undefined' && $(this).data('hasShortcutButton')) {
16100 // Otherwise, indicate that this link now has a shortcut button
16101 $(this).data('hasShortcutButton', true);
16103 var subreddit = $(this).find('a.title').attr('href').match(/^https?:\/\/(?:[a-z]+).reddit.com\/r\/([\w]+).*/i)[1],
16104 $theSC = $('<span>')
16105 .css({ display: 'inline-block', 'margin-right': '0'})
16106 .addClass('RESshortcut RESshortcutside')
16107 .data('subreddit', subreddit),
16108 isShortcut = false;
16110 for (var j = 0, shortcutsLength = modules['subredditManager'].mySubredditShortcuts.length; j < shortcutsLength; j++) {
16111 if (modules['subredditManager'].mySubredditShortcuts[j].subreddit === subreddit) {
16119 .attr('title', 'Remove this subreddit from your shortcut bar')
16121 .addClass('remove');
16124 .attr('title', 'Add this subreddit to your shortcut bar')
16126 .removeClass('remove');
16130 .on('click', modules['subredditManager'].toggleSubredditShortcut)
16131 .appendTo($(this).find('.midcol'));
16134 redrawShortcuts: function() {
16135 this.shortCutsContainer.textContent = '';
16136 // Try Refresh subreddit shortcuts
16137 if (this.mySubredditShortcuts.length === 0) {
16138 this.getLatestShortcuts();
16140 if (this.mySubredditShortcuts.length > 0) {
16141 // go through the list of shortcuts and print them out...
16142 for (var i = 0, len = this.mySubredditShortcuts.length; i < len; i++) {
16143 if (typeof this.mySubredditShortcuts[i] === 'string') {
16144 this.mySubredditShortcuts[i] = {
16145 subreddit: this.mySubredditShortcuts[i],
16146 displayName: this.mySubredditShortcuts[i],
16147 addedDate: new Date()
16151 var thisShortCut = document.createElement('a');
16152 thisShortCut.setAttribute('draggable', 'true');
16153 thisShortCut.setAttribute('orderIndex', i);
16154 thisShortCut.setAttribute('data-subreddit', this.mySubredditShortcuts[i].subreddit);
16155 thisShortCut.classList.add('subbarlink');
16157 if ((RESUtils.currentSubreddit() !== null) && (RESUtils.currentSubreddit().toLowerCase() === this.mySubredditShortcuts[i].subreddit.toLowerCase())) {
16158 thisShortCut.classList.add('RESShortcutsCurrentSub');
16161 thisShortCut.setAttribute('href','/r/'+this.mySubredditShortcuts[i].subreddit);
16162 thisShortCut.textContent = this.mySubredditShortcuts[i].displayName;
16163 thisShortCut.addEventListener('click', function(e) {
16164 if (e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey) {
16165 // open in new tab, let the browser handle it
16168 e.preventDefault();
16169 // use to open links in new tabs... work on this later...
16170 modules['subredditManager'].clickedShortcut = e.target.getAttribute('href');
16171 if (typeof modules['subredditManager'].clickTimer === 'undefined') {
16172 modules['subredditManager'].clickTimer = setTimeout(modules['subredditManager'].followSubredditShortcut, 300);
16177 thisShortCut.addEventListener('dblclick', function(e) {
16178 e.preventDefault();
16179 clearTimeout(modules['subredditManager'].clickTimer);
16180 delete modules['subredditManager'].clickTimer;
16181 modules['subredditManager'].editSubredditShortcut(e.target);
16184 thisShortCut.addEventListener('mouseover', function(e) {
16185 clearTimeout(modules['subredditManager'].hideSubredditGroupDropdownTimer);
16186 if ((typeof e.target.getAttribute !== 'undefined') && (e.target.getAttribute('href').indexOf('+') !== -1)) {
16187 var subreddits = e.target.getAttribute('href').replace('/r/','').split('+');
16188 modules['subredditManager'].showSubredditGroupDropdown(subreddits, e.target);
16192 thisShortCut.addEventListener('mouseout', function(e) {
16193 modules['subredditManager'].hideSubredditGroupDropdownTimer = setTimeout(function() {
16194 modules['subredditManager'].hideSubredditGroupDropdown();
16198 thisShortCut.addEventListener('dragstart', modules['subredditManager'].subredditDragStart, false);
16199 thisShortCut.addEventListener('dragenter', modules['subredditManager'].subredditDragEnter, false)
16200 thisShortCut.addEventListener('dragover', modules['subredditManager'].subredditDragOver, false);
16201 thisShortCut.addEventListener('dragleave', modules['subredditManager'].subredditDragLeave, false);
16202 thisShortCut.addEventListener('drop', modules['subredditManager'].subredditDrop, false);
16203 thisShortCut.addEventListener('dragend', modules['subredditManager'].subredditDragEnd, false);
16204 this.shortCutsContainer.appendChild(thisShortCut);
16207 var sep = document.createElement('span');
16208 sep.setAttribute('class', 'separator');
16209 sep.textContent = '-';
16210 this.shortCutsContainer.appendChild(sep);
16213 if (this.mySubredditShortcuts.length === 0) {
16214 this.shortCutsContainer.style.textTransform = 'none';
16215 this.shortCutsContainer.textContent = 'add shortcuts from the my subreddits menu at left or click the button by the subreddit name, drag and drop to sort';
16217 this.shortCutsContainer.style.textTransform = '';
16220 this.shortCutsContainer.style.textTransform = 'none';
16221 this.shortCutsContainer.textContent = 'add shortcuts from the my subreddits menu at left or click the button by the subreddit name, drag and drop to sort';
16222 this.mySubredditShortcuts = [];
16225 showSubredditGroupDropdown: function(subreddits, obj) {
16226 if (typeof this.subredditGroupDropdown === 'undefined') {
16227 this.subredditGroupDropdown = createElementWithID('div', 'RESSubredditGroupDropdown');
16228 this.subredditGroupDropdownUL = document.createElement('ul');
16229 this.subredditGroupDropdown.appendChild(this.subredditGroupDropdownUL);
16230 document.body.appendChild(this.subredditGroupDropdown);
16232 this.subredditGroupDropdown.addEventListener('mouseout', function(e) {
16233 modules['subredditManager'].hideSubredditGroupDropdownTimer = setTimeout(function() {
16234 modules['subredditManager'].hideSubredditGroupDropdown();
16238 this.subredditGroupDropdown.addEventListener('mouseover', function(e) {
16239 clearTimeout(modules['subredditManager'].hideSubredditGroupDropdownTimer);
16242 this.groupDropdownVisible = true;
16245 $(this.subredditGroupDropdownUL).html('');
16247 for (var i=0, len=subreddits.length; i<len; i++) {
16248 var thisLI = $('<li><a href="/r/'+subreddits[i]+'">'+subreddits[i]+'</a></li>');
16249 $(this.subredditGroupDropdownUL).append(thisLI);
16252 var thisXY = RESUtils.getXYpos(obj);
16253 this.subredditGroupDropdown.style.top = (thisXY.y + 16) + 'px';
16254 // if fixed, override y to just be the height of the subreddit bar...
16255 // this.subredditGroupDropdown.style.position = 'fixed';
16256 // this.subredditGroupDropdown.style.top = '20px';
16257 this.subredditGroupDropdown.style.left = thisXY.x + 'px';
16258 this.subredditGroupDropdown.style.display = 'block';
16260 modules['styleTweaks'].setSRStyleToggleVisibility(false, "subredditGroupDropdown");
16263 hideSubredditGroupDropdown: function() {
16264 delete modules['subredditManager'].hideSubredditGroupDropdownTimer;
16265 if (this.subredditGroupDropdown) {
16266 this.subredditGroupDropdown.style.display = 'none';
16267 modules['styleTweaks'].setSRStyleToggleVisibility(true, "subredditGroupDropdown")
16270 editSubredditShortcut: function(ele) {
16271 var subreddit = ele.getAttribute('href').slice(3);
16274 for (var i=0, len=modules['subredditManager'].mySubredditShortcuts.length; i<len; i++) {
16275 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit == subreddit) {
16281 if (typeof this.editShortcutDialog === 'undefined') {
16282 this.editShortcutDialog = createElementWithID('div','editShortcutDialog');
16283 document.body.appendChild(this.editShortcutDialog);
16286 var thisForm = '<form name="editSubredditShortcut"> \
16287 <h3>Edit Shortcut</h3> \
16288 <div id="editShortcutClose" class="RESCloseButton">×</div> \
16289 <label for="subreddit">Subreddit:</label> \
16290 <input type="text" name="subreddit" value="' + subreddit + '" id="shortcut-subreddit"> \
16292 <label for="displayName">Display Name:</label> \
16293 <input type="text" name="displayName" value="' + ele.textContent + '" id="shortcut-displayname"> \
16294 <input type="hidden" name="idx" value="' + idx + '"> \
16295 <input type="button" name="shortcut-save" value="save" id="shortcut-save"> \
16297 $(this.editShortcutDialog).html(thisForm);
16299 this.subredditInput = this.editShortcutDialog.querySelector('input[name=subreddit]');
16300 this.displayNameInput = this.editShortcutDialog.querySelector('input[name=displayName]');
16302 this.subredditForm = this.editShortcutDialog.querySelector('FORM');
16303 this.subredditForm.addEventListener('submit', function(e) {
16304 e.preventDefault();
16307 this.saveButton = this.editShortcutDialog.querySelector('input[name=shortcut-save]');
16308 this.saveButton.addEventListener('click', function(e) {
16309 var idx = modules['subredditManager'].editShortcutDialog.querySelector('input[name=idx]').value;
16310 var subreddit = modules['subredditManager'].editShortcutDialog.querySelector('input[name=subreddit]').value;
16311 var displayName = modules['subredditManager'].editShortcutDialog.querySelector('input[name=displayName]').value;
16313 if ((subreddit === '') || (displayName === '')) {
16314 // modules['subredditManager'].mySubredditShortcuts.splice(idx,1);
16315 subreddit = modules['subredditManager'].mySubredditShortcuts[idx].subreddit;
16316 modules['subredditManager'].removeSubredditShortcut(subreddit);
16318 if (RESUtils.proEnabled()) {
16319 // store a delete for the old subreddit, and an add for the new.
16320 var oldsubreddit = modules['subredditManager'].mySubredditShortcuts[idx].subreddit;
16321 if (typeof modules['subredditManager'].RESPro === 'undefined') {
16322 if (RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser()) !== null) {
16323 var temp = safeJSON.parse(RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser()), 'RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser());
16325 var temp = { add: {}, del: {} };
16327 modules['subredditManager'].RESPro = temp;
16329 if (typeof modules['subredditManager'].RESPro.add === 'undefined') {
16330 modules['subredditManager'].RESPro.add = {}
16332 if (typeof modules['subredditManager'].RESPro.del === 'undefined') {
16333 modules['subredditManager'].RESPro.del = {}
16335 // add modules['subredditManager'] new subreddit next time we sync...
16336 modules['subredditManager'].RESPro.add[subreddit] = true;
16337 // delete the old one
16338 modules['subredditManager'].RESPro.del[oldsubreddit] = true;
16339 // make sure we don't run an add on the old subreddit next time we sync...
16340 if (typeof modules['subredditManager'].RESPro.add[oldsubreddit] !== 'undefined') delete modules['subredditManager'].RESPro.add[oldsubreddit];
16341 // make sure we don't run a delete on the new subreddit next time we sync...
16342 if (typeof modules['subredditManager'].RESPro.del[subreddit] !== 'undefined') delete modules['subredditManager'].RESPro.del[subreddit];
16343 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].RESPro));
16345 modules['subredditManager'].mySubredditShortcuts[idx] = {
16346 subreddit: subreddit,
16347 displayName: displayName,
16348 addedDate: new Date()
16351 modules['subredditManager'].saveLatestShortcuts();
16353 if (RESUtils.proEnabled()) {
16354 modules['RESPro'].saveModuleData('subredditManager');
16358 modules['subredditManager'].editShortcutDialog.style.display = 'none';
16359 modules['subredditManager'].redrawShortcuts();
16360 modules['subredditManager'].populateSubredditDropdown();
16363 // handle enter and escape keys in the dialog box...
16364 this.subredditInput.addEventListener('keyup', function(e) {
16365 if (e.keyCode === 27) {
16366 modules['subredditManager'].editShortcutDialog.style.display = 'none';
16367 modules['subredditManager'].editShortcutDialog.blur();
16368 } else if (e.keyCode === 13) {
16369 RESUtils.click(modules['subredditManager'].saveButton);
16372 this.displayNameInput.addEventListener('keyup', function(e) {
16373 if (e.keyCode === 27) {
16374 modules['subredditManager'].editShortcutDialog.style.display = 'none';
16375 modules['subredditManager'].editShortcutDialog.blur();
16376 } else if (e.keyCode === 13) {
16377 RESUtils.click(modules['subredditManager'].saveButton);
16381 var cancelButton = this.editShortcutDialog.querySelector('#editShortcutClose');
16382 cancelButton.addEventListener('click', function(e) {
16383 modules['subredditManager'].editShortcutDialog.style.display = 'none';
16386 this.editShortcutDialog.style.display = 'block';
16387 var thisLeft = Math.min(RESUtils.mouseX, window.innerWidth - 300);
16388 this.editShortcutDialog.style.left = thisLeft + 'px';
16390 setTimeout(function() {
16391 modules['subredditManager'].subredditInput.focus()
16394 followSubredditShortcut: function() {
16395 if (BrowserDetect.isFirefox()) {
16396 // stupid firefox... sigh...
16397 location.href = location.protocol + '//' + location.hostname + modules['subredditManager'].clickedShortcut;
16399 location.href = modules['subredditManager'].clickedShortcut;
16402 subredditDragStart: function(e) {
16403 clearTimeout(modules['subredditManager'].clickTimer);
16404 // Target (this) element is the source node.
16405 this.style.opacity = '0.4';
16406 modules['subredditManager'].shortCutsTrash.style.display = 'block';
16407 modules['subredditManager'].dragSrcEl = this;
16409 e.dataTransfer.effectAllowed = 'move';
16410 // because Safari is stupid, we have to do this.
16411 modules['subredditManager'].srDataTransfer = this.getAttribute('orderIndex') + ',' + $(this).data('subreddit');
16413 subredditDragEnter: function(e) {
16414 this.classList.add('srOver');
16417 subredditDragOver: function(e) {
16418 if (e.preventDefault) {
16419 e.preventDefault(); // Necessary. Allows us to drop.
16422 // See the section on the DataTransfer object.
16423 e.dataTransfer.dropEffect = 'move';
16426 subredditDragLeave: function(e) {
16427 this.classList.remove('srOver');
16430 subredditDrop: function(e) {
16431 // this/e.target is current target element.
16432 if (e.stopPropagation) {
16433 e.stopPropagation(); // Stops some browsers from redirecting.
16436 // Stops other browsers from redirecting.
16437 e.preventDefault();
16439 modules['subredditManager'].shortCutsTrash.style.display = 'none';
16440 // Don't do anything if dropping the same column we're dragging.
16441 if (modules['subredditManager'].dragSrcEl !== this) {
16442 if (e.target.getAttribute('id') !== 'RESShortcutsTrash') {
16443 // get the order index of the src and destination to swap...
16444 // var theData = e.dataTransfer.getData('text/html').split(',');
16445 var theData = modules['subredditManager'].srDataTransfer.split(',');
16446 var srcOrderIndex = parseInt(theData[0], 10);
16447 var srcSubreddit = modules['subredditManager'].mySubredditShortcuts[srcOrderIndex];
16448 var destOrderIndex = parseInt(this.getAttribute('orderIndex'), 10);
16449 var destSubreddit = modules['subredditManager'].mySubredditShortcuts[destOrderIndex];
16450 var rearranged = [];
16451 var rearrangedI = 0;
16453 for (var i = 0, len = modules['subredditManager'].mySubredditShortcuts.length; i < len; i++) {
16454 if ((i !== srcOrderIndex) && (i !== destOrderIndex)) {
16455 rearranged[rearrangedI] = modules['subredditManager'].mySubredditShortcuts[i];
16457 } else if (i === destOrderIndex) {
16458 if (destOrderIndex > srcOrderIndex) {
16459 // if dragging right, order dest first, src next.
16460 rearranged[rearrangedI] = destSubreddit;
16462 rearranged[rearrangedI] = srcSubreddit;
16465 // if dragging left, order src first, dest next.
16466 rearranged[rearrangedI] = srcSubreddit;
16468 rearranged[rearrangedI] = destSubreddit;
16474 // save the updated order...
16475 modules['subredditManager'].mySubredditShortcuts = rearranged;
16476 modules['subredditManager'].saveLatestShortcuts();
16477 // redraw the shortcut bar...
16478 modules['subredditManager'].redrawShortcuts();
16479 this.classList.remove('srOver');
16481 var theData = modules['subredditManager'].srDataTransfer.split(',');
16482 var srcOrderIndex = parseInt(theData[0], 10);
16483 var srcSubreddit = theData[1];
16484 modules['subredditManager'].removeSubredditShortcut(srcSubreddit);
16489 subredditDragEnd: function(e) {
16490 modules['subredditManager'].shortCutsTrash.style.display = 'none';
16491 this.style.opacity = '1';
16494 redrawSubredditBar: function() {
16495 this.headerContents = document.querySelector('#sr-header-area');
16496 if (this.headerContents) {
16497 // for opera, because it renders progressively and makes it look "glitchy", hide the header bar, then show it all at once with CSS.
16498 // if (BrowserDetect.isOpera()) this.headerContents.style.display = 'none';
16499 // Clear out the existing stuff in the top bar first, we'll replace it with our own stuff.
16500 $(this.headerContents).html('');
16502 this.srLeftContainer = createElementWithID('div','srLeftContainer');
16503 this.srLeftContainer.setAttribute('class','sr-bar');
16505 this.srDropdown = createElementWithID('div','srDropdown');
16506 this.srDropdownContainer = createElementWithID('div','srDropdownContainer');
16507 $(this.srDropdownContainer).html('<a href="javascript:void(0)">My Subreddits</a>');
16508 this.srDropdownContainer.addEventListener('click',modules['subredditManager'].toggleSubredditDropdown, false);
16509 this.srDropdown.appendChild(this.srDropdownContainer);
16511 this.srList = createElementWithID('table','srList');
16512 var maxHeight = $(window).height() - 40;
16513 $(this.srList).css('max-height', maxHeight + 'px');
16514 // this.srDropdownContainer.appendChild(this.srList);
16515 document.body.appendChild(this.srList);
16517 this.srLeftContainer.appendChild(this.srDropdown);
16518 var sep = document.createElement('span');
16519 sep.setAttribute('class','srSep');
16520 sep.textContent = '|';
16521 this.srLeftContainer.appendChild(sep);
16523 // now put in the shortcuts...
16524 this.staticShortCutsContainer = document.createElement('div');
16525 this.staticShortCutsContainer.setAttribute('id','RESStaticShortcuts');
16526 /* this probably isn't the best way to give the option, since the mechanic is drag/drop for other stuff.. but it's much easier for now... */
16527 $(this.staticShortCutsContainer).html('');
16528 var specialButtonSelected = {};
16529 var subLower = (RESUtils.currentSubreddit()) ? RESUtils.currentSubreddit().toLowerCase() : 'home';
16530 specialButtonSelected[subLower] = 'RESShortcutsCurrentSub';
16532 var shortCutsHTML = '';
16534 if (this.options.linkDashboard.value) shortCutsHTML += '<span class="separator">-</span><a id="RESDashboardLink" class="subbarlink '+specialButtonSelected['dashboard']+'" href="/r/Dashboard/">DASHBOARD</a>';
16535 if (this.options.linkFront.value) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink '+specialButtonSelected['home']+'" href="/">FRONT</a>';
16536 if (this.options.linkAll.value) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink '+specialButtonSelected['all']+'" href="/r/all/">ALL</a>';
16537 if (this.options.linkRandom.value) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink" href="/r/random/">RANDOM</a>';
16538 if (this.options.linkMyRandom.value && this.myRandomEnabled) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink '+(this.myRandomGold ? 'gold' : '') + '" href="/r/myrandom/">MYRANDOM</a>';
16539 if (this.options.linkRandNSFW.value) shortCutsHTML += '<span class="separator over18">-</span><a class="subbarlink over18" href="/r/randnsfw/">RANDNSFW</a>';
16541 if (RESUtils.loggedInUser()) {
16542 if (this.options.linkFriends.value) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink '+specialButtonSelected['friends']+'" href="/r/friends/">FRIENDS</a>';
16544 var modmail = document.getElementById('modmail');
16546 if (this.options.linkMod.value) shortCutsHTML += '<span class="separator">-</span><a class=" '+specialButtonSelected['mod']+'" href="/r/mod/">MOD</a>';
16547 if (this.options.linkModqueue.value) shortCutsHTML += '<span class="separator">-</span><a class="subbarlink" href="/r/mod/about/modqueue">MODQUEUE</a>';
16550 $(this.staticShortCutsContainer).append(shortCutsHTML);
16552 this.srLeftContainer.appendChild(this.staticShortCutsContainer);
16553 this.srLeftContainer.appendChild(sep);
16554 this.headerContents.appendChild(this.srLeftContainer);
16556 this.shortCutsViewport = document.createElement('div');
16557 this.shortCutsViewport.setAttribute('id','RESShortcutsViewport');
16558 this.headerContents.appendChild(this.shortCutsViewport);
16560 this.shortCutsContainer = document.createElement('div');
16561 this.shortCutsContainer.setAttribute('id','RESShortcuts');
16562 this.shortCutsContainer.setAttribute('class','sr-bar');
16563 this.shortCutsViewport.appendChild(this.shortCutsContainer);
16565 this.shortCutsEditContainer = document.createElement('div');
16566 this.shortCutsEditContainer.setAttribute('id','RESShortcutsEditContainer');
16567 this.headerContents.appendChild(this.shortCutsEditContainer);
16569 // Add shortcut sorting arrow
16570 this.sortShortcutsButton = document.createElement('div');
16571 this.sortShortcutsButton.setAttribute('id','RESShortcutsSort');
16572 this.sortShortcutsButton.setAttribute('title','sort subreddit shortcuts');
16573 this.sortShortcutsButton.innerHTML = '↑↓';
16574 this.sortShortcutsButton.addEventListener('click', modules['subredditManager'].showSortMenu, false);
16575 this.shortCutsEditContainer.appendChild(this.sortShortcutsButton);
16577 // add right scroll arrow...
16578 this.shortCutsRight = document.createElement('div');
16579 this.shortCutsRight.setAttribute('id','RESShortcutsRight');
16580 this.shortCutsRight.textContent = '>';
16581 this.shortCutsRight.addEventListener('click', function(e) {
16582 modules['subredditManager'].containerWidth = modules['subredditManager'].shortCutsContainer.offsetWidth;
16583 var marginLeft = modules['subredditManager'].shortCutsContainer.firstChild.style.marginLeft;
16584 marginLeft = parseInt(marginLeft.replace('px', ''), 10);
16586 if (isNaN(marginLeft)) marginLeft = 0;
16588 var shiftWidth = $('#RESShortcutsViewport').width() - 80;
16589 if (modules['subredditManager'].containerWidth > (shiftWidth)) {
16590 marginLeft -= shiftWidth;
16591 modules['subredditManager'].shortCutsContainer.firstChild.style.marginLeft = marginLeft + 'px';
16594 this.shortCutsEditContainer.appendChild(this.shortCutsRight);
16596 // add an "add shortcut" button...
16597 this.shortCutsAdd = document.createElement('div');
16598 this.shortCutsAdd.setAttribute('id','RESShortcutsAdd');
16599 this.shortCutsAdd.textContent = '+';
16600 this.shortCutsAdd.title = 'add shortcut';
16601 this.shortCutsAddFormContainer = document.createElement('div');
16602 this.shortCutsAddFormContainer.setAttribute('id','RESShortcutsAddFormContainer');
16603 this.shortCutsAddFormContainer.style.display = 'none';
16605 <form id="shortCutsAddForm"> \
16606 <div>Add shortcut or multi-reddit (i.e. foo+bar+baz):</div> \
16607 <label for="newShortcut">Subreddit:</label> <input type="text" id="newShortcut"><br> \
16608 <label for="displayName">Display Name:</label> <input type="text" id="displayName"><br> \
16609 <input type="submit" name="submit" value="add" id="addSubreddit"> \
16610 <div style="clear: both; float: right; margin-top: 5px;"><a style="font-size: 9px;" href="/subreddits/">Edit frontpage subscriptions</a></div> \
16613 $(this.shortCutsAddFormContainer).html(thisForm);
16614 this.shortCutsAddFormField = this.shortCutsAddFormContainer.querySelector('#newShortcut');
16615 this.shortCutsAddFormFieldDisplayName = this.shortCutsAddFormContainer.querySelector('#displayName');
16617 modules['subredditManager'].shortCutsAddFormField.addEventListener('keyup', function(e) {
16618 if (e.keyCode === 27) {
16619 modules['subredditManager'].shortCutsAddFormContainer.style.display = 'none';
16620 modules['subredditManager'].shortCutsAddFormField.blur();
16624 modules['subredditManager'].shortCutsAddFormFieldDisplayName.addEventListener('keyup', function(e) {
16625 if (e.keyCode === 27) {
16626 modules['subredditManager'].shortCutsAddFormContainer.style.display = 'none';
16627 modules['subredditManager'].shortCutsAddFormFieldDisplayName.blur();
16631 // add the "add shortcut" form...
16632 this.shortCutsAddForm = this.shortCutsAddFormContainer.querySelector('#shortCutsAddForm');
16633 this.shortCutsAddForm.addEventListener('submit', function(e) {
16634 e.preventDefault();
16635 var subreddit = modules['subredditManager'].shortCutsAddFormField.value;
16636 var displayname = modules['subredditManager'].shortCutsAddFormFieldDisplayName.value;
16637 if (displayname === '') displayname = subreddit;
16639 var r_match_regex = /^(\/r\/|r\/)(.*)/i;
16640 if(r_match_regex.test(subreddit)) {
16641 subreddit = subreddit.match(r_match_regex)[2];
16644 modules['subredditManager'].shortCutsAddFormField.value = '';
16645 modules['subredditManager'].shortCutsAddFormFieldDisplayName.value = '';
16646 modules['subredditManager'].shortCutsAddFormContainer.style.display = 'none';
16649 modules['subredditManager'].addSubredditShortcut(subreddit, displayname);
16652 this.shortCutsAdd.addEventListener('click', function(e) {
16653 if (modules['subredditManager'].shortCutsAddFormContainer.style.display === 'none') {
16654 modules['subredditManager'].shortCutsAddFormContainer.style.display = 'block';
16655 modules['subredditManager'].shortCutsAddFormField.focus();
16657 modules['subredditManager'].shortCutsAddFormContainer.style.display = 'none';
16658 modules['subredditManager'].shortCutsAddFormField.blur();
16661 this.shortCutsEditContainer.appendChild(this.shortCutsAdd);
16662 document.body.appendChild(this.shortCutsAddFormContainer);
16664 // add the "trash bin"...
16665 this.shortCutsTrash = document.createElement('div');
16666 this.shortCutsTrash.setAttribute('id','RESShortcutsTrash');
16667 this.shortCutsTrash.textContent = '×';
16668 this.shortCutsTrash.addEventListener('dragenter', modules['subredditManager'].subredditDragEnter, false)
16669 this.shortCutsTrash.addEventListener('dragleave', modules['subredditManager'].subredditDragLeave, false);
16670 this.shortCutsTrash.addEventListener('dragover', modules['subredditManager'].subredditDragOver, false);
16671 this.shortCutsTrash.addEventListener('drop', modules['subredditManager'].subredditDrop, false);
16672 this.shortCutsEditContainer.appendChild(this.shortCutsTrash);
16674 // add left scroll arrow...
16675 this.shortCutsLeft = document.createElement('div');
16676 this.shortCutsLeft.setAttribute('id','RESShortcutsLeft');
16677 this.shortCutsLeft.textContent = '<';
16678 this.shortCutsLeft.addEventListener('click', function(e) {
16679 var marginLeft = modules['subredditManager'].shortCutsContainer.firstChild.style.marginLeft;
16680 marginLeft = parseInt(marginLeft.replace('px', ''), 10);
16682 if (isNaN(marginLeft)) marginLeft = 0;
16684 var shiftWidth = $('#RESShortcutsViewport').width() - 80;
16685 marginLeft += shiftWidth;
16686 if (marginLeft <= 0) {
16687 modules['subredditManager'].shortCutsContainer.firstChild.style.marginLeft = marginLeft + 'px';
16690 this.shortCutsEditContainer.appendChild(this.shortCutsLeft);
16692 this.redrawShortcuts();
16695 showSortMenu: function() {
16696 // Add shortcut sorting menu if it doesn't exist in the DOM yet...
16697 if (!modules['subredditManager'].sortMenu) {
16698 modules['subredditManager'].sortMenu =
16699 $('<div id="sort-menu" class="drop-choices">' +
16700 '<p> sort by:</p>' +
16701 '<a class="choice" data-field="displayName" href="javascript:void(0);">display name</a>' +
16702 '<a class="choice" data-field="addedDate" href="javascript:void(0);">added date</a>' +
16705 $(modules['subredditManager'].sortMenu).find('a').click(modules['subredditManager'].sortShortcuts);
16707 $(document.body).append(modules['subredditManager'].sortMenu);
16709 var menu = modules['subredditManager'].sortMenu;
16710 if ($(menu).is(':visible')) {
16714 var thisXY = $(modules['subredditManager'].sortShortcutsButton).offset();
16715 thisXY.left = thisXY.left - $(menu).width() + $(modules['subredditManager'].sortShortcutsButton).width();
16716 var thisHeight = $(modules['subredditManager'].sortShortcutsButton).height();
16719 top: thisXY.top + thisHeight,
16723 hideSortMenu: function() {
16724 var menu = modules['subredditManager'].sortMenu;
16727 sortShortcuts: function(e) {
16728 modules['subredditManager'].hideSortMenu();
16730 var sortingField = $(this).data('field');
16731 var asc = ! modules['subredditManager'].currentSort;
16732 // toggle sort method...
16733 modules['subredditManager'].currentSort = !modules['subredditManager'].currentSort;
16734 // Make sure we have a valid list of shortucts
16735 if (!modules['subredditManager'].mySubredditShortcuts) {
16736 modules['subredditManager'].getLatestShortcuts();
16739 modules['subredditManager'].mySubredditShortcuts = modules['subredditManager'].mySubredditShortcuts.sort(function(a, b) {
16740 // var sortingField = field; // modules['subredditManager'].options.sortingField.value;
16741 // var asc = order === 'asc'; // (modules['subredditManager'].options.sortingDirection.value === 'asc');
16742 var aField = a[sortingField];
16743 var bField = b[sortingField];
16744 if (typeof aField === 'string' && typeof bField === 'string') {
16745 aField = aField.toLowerCase();
16746 bField = bField.toLowerCase();
16749 if (aField === bField) {
16751 } else if (aField > bField) {
16752 return (asc) ? 1 : -1;
16754 return (asc) ? -1 : 1;
16758 // Save shortcuts sort order
16759 modules['subredditManager'].saveLatestShortcuts();
16761 // Refresh shortcuts
16762 modules['subredditManager'].redrawShortcuts();
16764 toggleSubredditDropdown: function(e) {
16765 e.stopPropagation();
16766 if (modules['subredditManager'].srList.style.display === 'block') {
16767 modules['subredditManager'].srList.style.display = 'none';
16768 document.body.removeEventListener('click',modules['subredditManager'].toggleSubredditDropdown, false);
16770 if (RESUtils.loggedInUser()) {
16771 $(modules['subredditManager'].srList).html('<tr><td width="360">Loading subreddits (may take a moment)...<div id="subredditPagesLoaded"></div></td></tr>');
16772 if (!modules['subredditManager'].subredditPagesLoaded) {
16773 modules['subredditManager'].subredditPagesLoaded = modules['subredditManager'].srList.querySelector('#subredditPagesLoaded');
16775 modules['subredditManager'].srList.style.display = 'block';
16776 modules['subredditManager'].getSubreddits();
16778 $(modules['subredditManager'].srList).html('<tr><td width="360">You must be logged in to load your own list of subreddits. <a style="display: inline; float: left;" href="/subreddits/">browse them all</a></td></tr>');
16779 modules['subredditManager'].srList.style.display = 'block';
16781 modules['subredditManager'].srList.addEventListener('click',modules['subredditManager'].stopDropDownPropagation, false);
16782 document.body.addEventListener('click',modules['subredditManager'].toggleSubredditDropdown, false);
16785 stopDropDownPropagation: function(e) {
16786 e.stopPropagation();
16790 mySubredditShortcuts: [
16792 getSubredditJSON: function(after) {
16793 var jsonURL = location.protocol + '//' + location.hostname + '/subreddits/mine/.json?app=res';
16794 if (after) jsonURL += '&after='+after;
16795 GM_xmlhttpRequest({
16798 onload: function(response) {
16799 var thisResponse = JSON.parse(response.responseText);
16800 if ((typeof thisResponse.data !== 'undefined') && (typeof thisResponse.data.children !== 'undefined')) {
16801 if (modules['subredditManager'].subredditPagesLoaded.innerHTML === '') {
16802 modules['subredditManager'].subredditPagesLoaded.textContent = 'Pages loaded: 1';
16804 var pages = modules['subredditManager'].subredditPagesLoaded.innerHTML.match(/:\ ([\d]+)/);
16805 modules['subredditManager'].subredditPagesLoaded.textContent = 'Pages loaded: ' + (parseInt(pages[1], 10)+1);
16808 var now = new Date();
16809 RESStorage.setItem('RESmodules.subredditManager.subreddits.lastCheck.'+RESUtils.loggedInUser(),now.getTime());
16811 var subreddits = thisResponse.data.children;
16812 for (var i = 0, len = subreddits.length; i < len; i++) {
16814 display_name: subreddits[i].data.display_name,
16815 url: subreddits[i].data.url,
16816 over18: subreddits[i].data.over18,
16817 id: subreddits[i].data.id,
16818 created: subreddits[i].data.created,
16819 description: subreddits[i].data.description
16821 modules['subredditManager'].mySubreddits.push(srObj);
16824 if (thisResponse.data.after) {
16825 modules['subredditManager'].getSubredditJSON(thisResponse.data.after);
16827 modules['subredditManager'].mySubreddits.sort(function(a, b) {
16828 var adisp = a.display_name.toLowerCase();
16829 var bdisp = b.display_name.toLowerCase();
16830 if (adisp > bdisp) return 1;
16831 if (adisp == bdisp) return 0;
16835 RESStorage.setItem('RESmodules.subredditManager.subreddits.' + RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].mySubreddits));
16836 this.gettingSubreddits = false;
16837 modules['subredditManager'].populateSubredditDropdown();
16840 // User is probably not logged in.. no subreddits found.
16841 modules['subredditManager'].populateSubredditDropdown(null, true);
16847 getSubreddits: function() {
16848 modules['subredditManager'].mySubreddits = [];
16849 var lastCheck = parseInt(RESStorage.getItem('RESmodules.subredditManager.subreddits.lastCheck.'+RESUtils.loggedInUser()), 10) || 0;
16850 var now = new Date();
16851 var check = RESStorage.getItem('RESmodules.subredditManager.subreddits.'+RESUtils.loggedInUser());
16853 // 86400000 = 1 day
16854 if (((now.getTime() - lastCheck) > 86400000) || !check || (check.length === 0)) {
16855 if (!this.gettingSubreddits) {
16856 this.gettingSubreddits = true;
16857 this.getSubredditJSON();
16860 modules['subredditManager'].mySubreddits = safeJSON.parse(check, 'RESmodules.subredditManager.subreddits.'+RESUtils.loggedInUser());
16861 this.populateSubredditDropdown();
16864 // if badJSON is true, then getSubredditJSON ran into an error...
16865 populateSubredditDropdown: function(sortBy, badJSON) {
16866 modules['subredditManager'].sortBy = sortBy || 'subreddit';
16867 $(modules['subredditManager'].srList).html('');
16868 // NOTE WE NEED TO CHECK LAST TIME THEY UPDATED THEIR SUBREDDIT LIST AND REPOPULATE...
16870 var theHead = document.createElement('thead');
16871 var theRow = document.createElement('tr');
16873 modules['subredditManager'].srHeader = document.createElement('td');
16874 modules['subredditManager'].srHeader.addEventListener('click', function() {
16875 if (modules['subredditManager'].sortBy === 'subreddit') {
16876 modules['subredditManager'].populateSubredditDropdown('subredditDesc');
16878 modules['subredditManager'].populateSubredditDropdown('subreddit');
16881 modules['subredditManager'].srHeader.textContent = 'subreddit';
16882 modules['subredditManager'].srHeader.setAttribute('width','200');
16884 modules['subredditManager'].lvHeader = document.createElement('td');
16885 modules['subredditManager'].lvHeader.addEventListener('click', function() {
16886 if (modules['subredditManager'].sortBy === 'lastVisited') {
16887 modules['subredditManager'].populateSubredditDropdown('lastVisitedAsc');
16889 modules['subredditManager'].populateSubredditDropdown('lastVisited');
16892 modules['subredditManager'].lvHeader.textContent = 'Last Visited';
16893 modules['subredditManager'].lvHeader.setAttribute('width','120');
16895 var scHeader = document.createElement('td');
16896 $(scHeader).width(50);
16897 $(scHeader).html('<a style="float: right;" href="/subreddits/">View all »</a>');
16898 theRow.appendChild(modules['subredditManager'].srHeader);
16899 theRow.appendChild(modules['subredditManager'].lvHeader);
16900 theRow.appendChild(scHeader);
16901 theHead.appendChild(theRow);
16902 modules['subredditManager'].srList.appendChild(theHead);
16904 var theBody = document.createElement('tbody');
16906 var subredditCount = modules['subredditManager'].mySubreddits.length;
16908 if (typeof this.subredditsLastViewed === 'undefined') {
16909 var check = RESStorage.getItem('RESmodules.subredditManager.subredditsLastViewed.'+RESUtils.loggedInUser());
16911 this.subredditsLastViewed = safeJSON.parse(check, 'RESmodules.subredditManager.subredditsLastViewed.'+RESUtils.loggedInUser());
16913 this.subredditsLastViewed = {};
16917 // copy modules['subredditManager'].mySubreddits to a placeholder array so we can sort without modifying it...
16918 var sortableSubreddits = modules['subredditManager'].mySubreddits;
16919 if (sortBy === 'lastVisited') {
16920 $(modules['subredditManager'].lvHeader).html('Last Visited <div class="sortAsc"></div>');
16921 modules['subredditManager'].srHeader.textContent = 'subreddit';
16923 sortableSubreddits.sort(function(a, b) {
16924 var adisp = a.display_name.toLowerCase();
16925 var bdisp = b.display_name.toLowerCase();
16927 (typeof modules['subredditManager'].subredditsLastViewed[adisp] === 'undefined') ? alv = 0 : alv = parseInt(modules['subredditManager'].subredditsLastViewed[adisp].last_visited, 10);
16928 (typeof modules['subredditManager'].subredditsLastViewed[bdisp] === 'undefined') ? blv = 0 : blv = parseInt(modules['subredditManager'].subredditsLastViewed[bdisp].last_visited, 10);
16930 if (alv < blv) return 1;
16932 if (adisp > bdisp) return 1;
16937 } else if (sortBy === 'lastVisitedAsc') {
16938 $(modules['subredditManager'].lvHeader).html('Last Visited <div class="sortDesc"></div>');
16939 modules['subredditManager'].srHeader.textContent = 'subreddit';
16941 sortableSubreddits.sort(function(a, b) {
16942 var adisp = a.display_name.toLowerCase();
16943 var bdisp = b.display_name.toLowerCase();
16945 (typeof modules['subredditManager'].subredditsLastViewed[adisp] === 'undefined') ? alv = 0 : alv = parseInt(modules['subredditManager'].subredditsLastViewed[adisp].last_visited, 10);
16946 (typeof modules['subredditManager'].subredditsLastViewed[bdisp] === 'undefined') ? blv = 0 : blv = parseInt(modules['subredditManager'].subredditsLastViewed[bdisp].last_visited, 10);
16948 if (alv > blv) return 1;
16950 if (adisp > bdisp) return 1;
16955 } else if (sortBy === 'subredditDesc') {
16956 modules['subredditManager'].lvHeader.textContent = 'Last Visited';
16957 $(modules['subredditManager'].srHeader).html('subreddit <div class="sortDesc"></div>');
16959 sortableSubreddits.sort(function(a,b) {
16960 var adisp = a.display_name.toLowerCase();
16961 var bdisp = b.display_name.toLowerCase();
16963 if (adisp < bdisp) return 1;
16964 if (adisp == bdisp) return 0;
16968 modules['subredditManager'].lvHeader.textContent = 'Last Visited';
16969 $(modules['subredditManager'].srHeader).html('subreddit <div class="sortAsc"></div>');
16971 sortableSubreddits.sort(function(a,b) {
16972 var adisp = a.display_name.toLowerCase();
16973 var bdisp = b.display_name.toLowerCase();
16975 if (adisp > bdisp) return 1;
16976 if (adisp == bdisp) return 0;
16980 for (var i=0; i<subredditCount; i++) {
16981 var dateString = 'Never';
16982 var thisReddit = sortableSubreddits[i].display_name.toLowerCase();
16983 if (typeof this.subredditsLastViewed[thisReddit] !== 'undefined') {
16984 var ts = parseInt(this.subredditsLastViewed[thisReddit].last_visited, 10);
16985 var dateVisited = new Date(ts);
16986 dateString = RESUtils.niceDate(dateVisited);
16989 var theRow = document.createElement('tr');
16990 var theSR = document.createElement('td');
16991 $(theSR).html('<a href="'+escapeHTML(modules['subredditManager'].mySubreddits[i].url)+'">'+escapeHTML(modules['subredditManager'].mySubreddits[i].display_name)+'</a>');
16992 theRow.appendChild(theSR);
16994 var theLV = document.createElement('td');
16995 theLV.textContent = dateString;
16996 theLV.setAttribute('class','RESvisited');
16997 theRow.appendChild(theLV);
16999 var theSC = document.createElement('td');
17000 theSC.setAttribute('class','RESshortcut');
17001 theSC.setAttribute('data-subreddit',modules['subredditManager'].mySubreddits[i].display_name);
17004 for (var j = 0, len = modules['subredditManager'].mySubredditShortcuts.length; j < len; j++) {
17005 if (modules['subredditManager'].mySubredditShortcuts[j].subreddit === modules['subredditManager'].mySubreddits[i].display_name) {
17012 theSC.addEventListener('click', function(e) {
17013 if (e.stopPropagation) {
17014 e.stopPropagation(); // Stops from triggering the click on the bigger box, which toggles this window closed...
17017 var subreddit = $(e.target).data('subreddit');
17018 modules['subredditManager'].removeSubredditShortcut(subreddit);
17021 theSC.textContent = '-shortcut';
17023 theSC.addEventListener('click', function(e) {
17024 if (e.stopPropagation) {
17025 e.stopPropagation(); // Stops from triggering the click on the bigger box, which toggles this window closed...
17028 var subreddit = $(e.target).data('subreddit');
17029 modules['subredditManager'].addSubredditShortcut(subreddit);
17032 theSC.textContent = '+shortcut';
17035 theRow.appendChild(theSC);
17036 theBody.appendChild(theRow);
17039 var theTD = document.createElement('td');
17040 theTD.textContent = 'There was an error getting your subreddits. You may have third party cookies disabled by your browser. For this function to work, you\'ll need to add an exception for cookies from reddit.com';
17041 theTD.setAttribute('colspan','3');
17043 var theRow = document.createElement('tr');
17044 theRow.appendChild(theTD);
17045 theBody.appendChild(theRow);
17048 modules['subredditManager'].srList.appendChild(theBody);
17050 toggleSubredditShortcut: function(e) {
17051 e.stopPropagation(); // Stops from triggering the click on the bigger box, which toggles this window closed...
17053 var isShortcut = false;
17054 for (var i = 0, len = modules['subredditManager'].mySubredditShortcuts.length; i < len; i++) {
17055 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === $(this).data('subreddit').toLowerCase()) {
17062 modules['subredditManager'].removeSubredditShortcut($(this).data('subreddit'));
17064 .attr('title', 'Add this subreddit to your shortcut bar')
17066 .removeClass('remove');
17068 modules['subredditManager'].addSubredditShortcut($(this).data('subreddit'));
17070 .attr('title', 'Remove this subreddit from your shortcut bar')
17072 .addClass('remove');
17075 modules['subredditManager'].redrawShortcuts();
17077 getLatestShortcuts: function() {
17078 // re-retreive the latest data to ensure we're not losing info between tab changes...
17079 var shortCuts = RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.' + RESUtils.loggedInUser());
17084 this.mySubredditShortcuts = safeJSON.parse(shortCuts, 'RESmodules.subredditManager.subredditShortcuts.' + RESUtils.loggedInUser());
17087 // JSON specification doesn't specify what to do with dates - so unstringify here
17088 parseDates: function () {
17089 for (var i = 0, len = this.mySubredditShortcuts.length; i < len; i++) {
17090 this.mySubredditShortcuts[i].addedDate = this.mySubredditShortcuts[i].addedDate
17091 ? new Date(this.mySubredditShortcuts[i].addedDate)
17095 saveLatestShortcuts: function() {
17096 // Retreive the latest data to ensure we're not losing info
17097 if (!modules['subredditManager'].mySubredditShortcuts) {
17098 modules['subredditManager'].mySubredditShortcuts = [];
17101 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.' + RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].mySubredditShortcuts));
17103 addSubredditShortcut: function(subreddit, displayname) {
17104 modules['subredditManager'].getLatestShortcuts();
17107 for (var i = 0, len=modules['subredditManager'].mySubredditShortcuts.length; i < len; i++) {
17108 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === subreddit.toLowerCase()) {
17115 alert('Whoops, you already have a shortcut for that subreddit');
17117 displayname = displayname || subreddit;
17118 var subredditObj = {
17119 subreddit: subreddit,
17120 displayName: displayname.toLowerCase(),
17121 addedDate: new Date()
17124 modules['subredditManager'].mySubredditShortcuts.push(subredditObj);
17125 if (RESUtils.proEnabled()) {
17126 if (typeof modules['subredditManager'].RESPro === 'undefined') {
17127 if (RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser())) {
17128 var temp = safeJSON.parse(RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser()), 'RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser());
17130 var temp = { add: {}, del: {} };
17133 modules['subredditManager'].RESPro = temp;
17136 if (typeof modules['subredditManager'].RESPro.add === 'undefined') {
17137 modules['subredditManager'].RESPro.add = {}
17140 if (typeof modules['subredditManager'].RESPro.del === 'undefined') {
17141 modules['subredditManager'].RESPro.del = {}
17144 // add this subreddit next time we sync...
17145 modules['subredditManager'].RESPro.add[subreddit] = true;
17147 // make sure we don't run a delete on this subreddit next time we sync...
17148 if (typeof modules['subredditManager'].RESPro.del[subreddit] !== 'undefined') delete modules['subredditManager'].RESPro.del[subreddit];
17150 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].RESPro));
17153 modules['subredditManager'].saveLatestShortcuts();
17154 modules['subredditManager'].redrawShortcuts();
17155 modules['subredditManager'].populateSubredditDropdown();
17157 if (RESUtils.proEnabled()) {
17158 modules['RESPro'].saveModuleData('subredditManager');
17161 RESUtils.notification({
17162 moduleID: 'subredditManager',
17163 message: 'Subreddit shortcut added. You can edit by double clicking, or trash by dragging to the trash can.'
17167 removeSubredditShortcut: function(subreddit) {
17168 this.getLatestShortcuts();
17171 for (var i = 0, len = modules['subredditManager'].mySubredditShortcuts.length; i < len; i++) {
17172 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === subreddit.toLowerCase()) {
17179 modules['subredditManager'].mySubredditShortcuts.splice(idx, 1);
17181 if (RESUtils.proEnabled()) {
17182 if (typeof modules['subredditManager'].RESPro === 'undefined') {
17183 if (RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser())) {
17184 var temp = safeJSON.parse(RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser()), 'RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser());
17186 var temp = { add: {}, del: {} };
17189 modules['subredditManager'].RESPro = temp;
17191 if (typeof modules['subredditManager'].RESPro.add === 'undefined') {
17192 modules['subredditManager'].RESPro.add = {}
17194 if (typeof modules['subredditManager'].RESPro.del === 'undefined') {
17195 modules['subredditManager'].RESPro.del = {}
17198 // delete this subreddit next time we sync...
17199 modules['subredditManager'].RESPro.del[subreddit] = true;
17201 // make sure we don't run an add on this subreddit
17202 if (typeof modules['subredditManager'].RESPro.add[subreddit] !== 'undefined') delete modules['subredditManager'].RESPro.add[subreddit];
17204 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].RESPro));
17207 modules['subredditManager'].saveLatestShortcuts();
17208 modules['subredditManager'].redrawShortcuts();
17209 modules['subredditManager'].populateSubredditDropdown();
17211 if (RESUtils.proEnabled()) {
17212 modules['RESPro'].saveModuleData('subredditManager');
17216 setLastViewtime: function() {
17217 var check = RESStorage.getItem('RESmodules.subredditManager.subredditsLastViewed.' + RESUtils.loggedInUser());
17220 this.subredditsLastViewed = {};
17222 this.subredditsLastViewed = safeJSON.parse(check, 'RESmodules.subredditManager.subredditsLastViewed.' + RESUtils.loggedInUser());
17225 var now = new Date();
17226 var thisReddit = RESUtils.currentSubreddit().toLowerCase();
17227 this.subredditsLastViewed[thisReddit] = {
17228 last_visited: now.getTime()
17231 RESStorage.setItem('RESmodules.subredditManager.subredditsLastViewed.' + RESUtils.loggedInUser(), JSON.stringify(this.subredditsLastViewed));
17233 subscribeToSubreddit: function(subredditName, subscribe) {
17234 // subredditName should look like t5_123asd
17235 subscribe = subscribe !== false; // default to true
17236 var userHash = RESUtils.loggedInUserHash();
17238 var formData = new FormData();
17239 formData.append('sr', subredditName);
17240 formData.append('action', subscribe ? 'sub' : 'unsub');
17241 formData.append('uh', userHash);
17243 GM_xmlhttpRequest({
17245 url: location.protocol + "//"+location.hostname+"/api/subscribe?app=res",
17251 }; // note: you NEED this semicolon at the end!
17253 // RES Pro needs some work still... not ready yet.
17255 modules['RESPro'] = {
17256 moduleID: 'RESPro',
17257 moduleName: 'RES Pro',
17258 category: 'Pro Features',
17260 // any configurable options you have go here...
17261 // options must have a type and a value..
17262 // valid types are: text, boolean (if boolean, value must be true or false)
17267 description: 'Your RES Pro username'
17272 description: 'Your RES Pro password'
17277 { name: 'Hourly', value: '3600000' },
17278 { name: 'Daily', value: '86400000' },
17279 { name: 'Manual Only', value: '-1' }
17282 description: 'How often should RES automatically sync settings?'
17285 description: 'RES Pro allows you to sync settings and data to a server. It requires an account, which you can sign up for <a href="http://reddit.honestbleeps.com/register.php">here</a>',
17286 isEnabled: function() {
17287 return RESConsole.getModulePrefs(this.moduleID);
17290 /^https?:\/\/([a-z]+)\.reddit\.com\/?/i,
17291 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+/i
17293 isMatchURL: function() {
17294 return RESUtils.isMatchURL(this.moduleID);
17297 if ((this.isEnabled()) && (this.isMatchURL())) {
17299 // if we haven't synced in more than our settings, and settings != manual, sync!
17300 if (this.options.syncFrequency.value > 0) {
17301 var lastSync = parseInt(RESStorage.getItem('RESmodules.RESPro.lastSync')) || 0;
17302 var now = new Date();
17303 if ((now.getTime() - lastSync) > this.options.syncFrequency.value) {
17304 this.authenticate(this.autoSync);
17310 autoSync: function() {
17311 modules['RESPro'].authenticate(modules['RESPro'].savePrefs);
17313 // modules['RESPro'].authenticate(function() {
17314 // modules['RESPro'].saveModuleData('saveComments');
17317 saveModuleData: function(module) {
17320 // THIS IS NOT READY YET! We need to merge votes on the backend.. hard stuff...
17321 // in this case, we want to send the JSON from RESmodules.userTagger.tags;
17322 var tags = RESStorage.getItem('RESmodules.userTagger.tags');
17323 GM_xmlhttpRequest({
17325 url: 'http://reddit.honestbleeps.com/RESsync.php',
17326 data: 'action=PUT&type=module_data&module='+module+'&data='+tags,
17328 "Content-Type": "application/x-www-form-urlencoded"
17330 onload: function(response) {
17331 var resp = JSON.parse(response.responseText);
17332 // console.log(resp);
17333 if (resp.success) {
17334 if (RESConsole.proUserTaggerSaveButton) RESConsole.proUserTaggerSaveButton.textContent = 'Saved!';
17336 alert(response.responseText);
17341 case 'saveComments':
17342 var savedComments = RESStorage.getItem('RESmodules.saveComments.savedComments');
17343 GM_xmlhttpRequest({
17345 url: 'http://reddit.honestbleeps.com/RESsync.php',
17346 data: 'action=PUT&type=module_data&module='+module+'&data='+savedComments,
17348 "Content-Type": "application/x-www-form-urlencoded"
17350 onload: function(response) {
17351 // console.log(response.responseText);
17352 var resp = JSON.parse(response.responseText);
17353 if (resp.success) {
17354 if (RESConsole.proSaveCommentsSaveButton) RESConsole.proSaveCommentsSaveButton.textContent = 'Saved!';
17355 var thisComments = safeJSON.parse(savedComments);
17356 delete thisComments.RESPro_add;
17357 delete thisComments.RESPro_delete;
17358 thisComments = JSON.stringify(thisComments);
17359 RESStorage.setItem('RESmodules.saveComments.savedComments',thisComments);
17360 RESUtils.notification({
17361 header: 'RES Pro Notification',
17362 message: 'Saved comments synced to server'
17365 alert(response.responseText);
17370 case 'subredditManager':
17371 var subredditManagerData = {};
17372 subredditManagerData.RESPro = {};
17374 for (var key in RESStorage) {
17375 // console.log(key);
17376 if (key.indexOf('RESmodules.subredditManager') !== -1) {
17377 var keySplit = key.split('.');
17378 var username = keySplit[keySplit.length-1];
17379 if ((keySplit.indexOf('subredditsLastViewed') === -1) && (keySplit.indexOf('subreddits') === -1)) {
17380 // console.log(key);
17381 (keySplit.indexOf('RESPro') !== -1) ? subredditManagerData.RESPro[username] = JSON.parse(RESStorage[key]) : subredditManagerData[username] = JSON.parse(RESStorage[key]);
17382 // if (key.indexOf('RESPro') === -1) console.log(username + ' -- ' + RESStorage[key]);
17383 if (key.indexOf('RESPro') !== -1) RESStorage.removeItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+username);
17387 var stringData = JSON.stringify(subredditManagerData);
17388 stringData = encodeURIComponent(stringData);
17389 GM_xmlhttpRequest({
17391 url: 'http://reddit.honestbleeps.com/RESsync.php',
17392 data: 'action=PUT&type=module_data&module='+module+'&data='+stringData,
17394 "Content-Type": "application/x-www-form-urlencoded"
17396 onload: function(response) {
17397 console.log(response.responseText);
17398 var resp = JSON.parse(response.responseText);
17399 if (resp.success) {
17400 if (RESConsole.proSubredditManagerSaveButton) RESConsole.proSubredditManagerSaveButton.textContent = 'Saved!';
17401 RESUtils.notification({
17402 header: 'RES Pro Notification',
17403 message: 'Subreddit shortcuts synced to server'
17406 alert(response.responseText);
17412 console.log('invalid module specified: ' + module);
17416 getModuleData: function(module) {
17418 case 'saveComments':
17419 if (RESConsole.proSaveCommentsGetButton) RESConsole.proSaveCommentsGetButton.textContent = 'Loading...';
17420 GM_xmlhttpRequest({
17422 url: 'http://reddit.honestbleeps.com/RESsync.php',
17423 data: 'action=GET&type=module_data&module='+module,
17425 "Content-Type": "application/x-www-form-urlencoded"
17427 onload: function(response) {
17428 var resp = JSON.parse(response.responseText);
17429 if (resp.success) {
17430 var serverResponse = JSON.parse(response.responseText);
17431 var serverData = serverResponse.data;
17432 currentData = safeJSON.parse(RESStorage.getItem('RESmodules.saveComments.savedComments'), 'RESmodules.saveComments.savedComments');
17433 for (var attrname in serverData) {
17434 if (typeof currentData[attrname] === 'undefined') {
17435 currentData[attrname] = serverData[attrname];
17438 // console.log(JSON.stringify(prefsData));
17439 RESStorage.setItem('RESmodules.saveComments.savedComments', JSON.stringify(currentData));
17440 if (RESConsole.proSaveCommentsGetButton) RESConsole.proSaveCommentsGetButton.textContent = 'Saved Comments Loaded!';
17442 alert(response.responseText);
17447 case 'subredditManager':
17448 if (RESConsole.proSubredditManagerGetButton) RESConsole.proSubredditManagerGetButton.textContent = 'Loading...';
17449 GM_xmlhttpRequest({
17451 url: 'http://reddit.honestbleeps.com/RESsync.php',
17452 data: 'action=GET&type=module_data&module='+module,
17454 "Content-Type": "application/x-www-form-urlencoded"
17456 onload: function(response) {
17457 var resp = JSON.parse(response.responseText);
17458 if (resp.success) {
17459 var serverResponse = JSON.parse(response.responseText);
17460 var serverData = serverResponse.data;
17461 for (var username in serverResponse.data) {
17462 var newSubredditData = serverResponse.data[username];
17463 var oldSubredditData = safeJSON.parse(RESStorage.getItem('RESmodules.subredditManager.subredditShortcuts.'+username), 'RESmodules.subredditManager.subredditShortcuts.'+username);
17464 if (oldSubredditData == null) oldSubredditData = [];
17465 for (var newidx in newSubredditData) {
17466 var exists = false;
17467 for (var oldidx in oldSubredditData) {
17468 if (oldSubredditData[oldidx].subreddit == newSubredditData[newidx].subreddit) {
17469 oldSubredditData[oldidx].displayName = newSubredditData[newidx].displayName;
17475 oldSubredditData.push(newSubredditData[newidx]);
17478 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.'+username,JSON.stringify(oldSubredditData));
17481 alert(response.responseText);
17487 console.log('invalid module specified: ' + module);
17491 savePrefs: function() {
17492 // (typeof unsafeWindow !== 'undefined') ? ls = unsafeWindow.localStorage : ls = localStorage;
17493 if (RESConsole.proSaveButton) RESConsole.proSaveButton.textContent = 'Saving...';
17494 var RESOptions = {};
17495 // for (var i = 0, len=ls.length; i < len; i++) {
17496 for(var i in RESStorage) {
17497 if ((typeof RESStorage.getItem(i) !== 'function') && (typeof RESStorage.getItem(i) !== 'undefined')) {
17498 var keySplit = i.split('.');
17500 var keyRoot = keySplit[0];
17503 var thisNode = keySplit[1];
17504 if (thisNode === 'modulePrefs') {
17505 RESOptions[thisNode] = safeJSON.parse(RESStorage.getItem(i), i);
17509 var thisModule = keySplit[1];
17510 if (thisModule !== 'accountSwitcher') {
17511 RESOptions[thisModule] = safeJSON.parse(RESStorage.getItem(i), i);
17515 //console.log('Not currently handling keys with root: ' + keyRoot);
17521 // Post options blob.
17522 var RESOptionsString = JSON.stringify(RESOptions);
17523 GM_xmlhttpRequest({
17525 url: 'http://reddit.honestbleeps.com/RESsync.php',
17526 data: 'action=PUT&type=all_options&data='+RESOptionsString,
17528 "Content-Type": "application/x-www-form-urlencoded"
17530 onload: function(response) {
17531 var resp = JSON.parse(response.responseText);
17532 // console.log(resp);
17533 if (resp.success) {
17534 var now = new Date();
17535 RESStorage.setItem('RESmodules.RESPro.lastSync',now.getTime());
17536 if (RESConsole.proSaveButton) RESConsole.proSaveButton.textContent = 'Saved.';
17537 RESUtils.notification({
17538 header: 'RES Pro Notification',
17539 message: 'RES Pro - module options saved to server.'
17542 alert(response.responseText);
17547 getPrefs: function() {
17548 console.log('get prefs called');
17549 if (RESConsole.proGetButton) RESConsole.proGetButton.textContent = 'Loading...';
17550 GM_xmlhttpRequest({
17552 url: 'http://reddit.honestbleeps.com/RESsync.php',
17553 data: 'action=GET&type=all_options',
17555 "Content-Type": "application/x-www-form-urlencoded"
17557 onload: function(response) {
17558 var resp = JSON.parse(response.responseText);
17559 if (resp.success) {
17560 var modulePrefs = JSON.parse(response.responseText);
17561 var prefsData = modulePrefs.data;
17562 //console.log('prefsData:');
17563 //console.log(prefsData);
17564 for (var thisModule in prefsData){
17565 if (thisModule === 'modulePrefs') {
17566 var thisOptions = prefsData[thisModule];
17567 RESStorage.setItem('RES.modulePrefs',JSON.stringify(thisOptions));
17569 var thisOptions = prefsData[thisModule];
17570 RESStorage.setItem('RESoptions.'+thisModule,JSON.stringify(thisOptions));
17573 if (RESConsole.proGetButton) RESConsole.proGetButton.textContent = 'Preferences Loaded!';
17574 RESUtils.notification({
17575 header: 'RES Pro Notification',
17576 message: 'Module options loaded.'
17578 // console.log(response.responseText);
17580 alert(response.responseText);
17585 configure: function() {
17586 if (!RESConsole.isOpen) RESConsole.open();
17587 RESConsole.menuClick(document.getElementById('Menu-'+this.category));
17588 RESConsole.drawConfigOptions('RESPro');
17590 authenticate: function(callback) {
17591 if (! this.isEnabled()) {
17593 } else if ((modules['RESPro'].options.username.value === '') || (modules['RESPro'].options.password.value === '')) {
17594 modules['RESPro'].configure();
17595 } else if (RESStorage.getItem('RESmodules.RESPro.lastAuthFailed') !== 'true') {
17596 if (typeof modules['RESPro'].lastAuthFailed === 'undefined') {
17597 GM_xmlhttpRequest({
17599 url: 'http://reddit.honestbleeps.com/RESlogin.php',
17600 data: 'uname='+modules['RESPro'].options.username.value+'&pwd='+modules['RESPro'].options.password.value,
17602 "Content-Type": "application/x-www-form-urlencoded"
17604 onload: function(response) {
17605 var resp = JSON.parse(response.responseText);
17606 if (resp.success) {
17607 // RESConsole.proAuthButton.textContent = 'Authenticated!';
17608 RESStorage.removeItem('RESmodules.RESPro.lastAuthFailed');
17613 // RESConsole.proAuthButton.textContent = 'Authentication failed.';
17614 modules['RESPro'].lastAuthFailed = true;
17615 RESStorage.setItem('RESmodules.RESPro.lastAuthFailed','true');
17616 RESUtils.notification({
17617 header: 'RES Pro Notification',
17618 message: 'Authentication failed - check your username and password.'
17628 modules['RESTips'] = {
17629 moduleID: 'RESTips',
17630 moduleName: 'RES Tips and Tricks',
17633 // any configurable options you have go here...
17634 // options must have a type and a value..
17635 // valid types are: text, boolean (if boolean, value must be true or false)
17640 description: 'Show a random tip once every 24 hours.'
17643 description: 'Adds tips/tricks help to RES console',
17644 isEnabled: function() {
17645 return RESConsole.getModulePrefs(this.moduleID);
17648 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
17650 isMatchURL: function() {
17651 return RESUtils.isMatchURL(this.moduleID);
17653 beforeLoad: function() {
17654 if (this.isEnabled() && this.isMatchURL()) {
17655 RESUtils.addCSS('.res-help { cursor: help; }');
17656 RESUtils.addCSS('.res-help #resHelp { cursor: default; }');
17660 if ((this.isEnabled()) && (this.isMatchURL())) {
17661 this.menuItem = createElementWithID('li','RESTipsMenuItem');
17662 this.menuItem.textContent = 'tips & tricks';
17663 this.menuItem.addEventListener('click', function(e) {
17664 modules['RESTips'].randomTip();
17667 $('#RESDropdownOptions').append(this.menuItem);
17669 if (this.options.dailyTip.value) {
17674 guiders.createGuider({
17675 attachTo: '#RESSettingsButton',
17676 // buttons: [{name: "Next"}],
17677 description: "Guiders are a user interface design pattern for introducing features of software. This dialog box, for example, is the first in a series of guiders that together make up a guide.",
17682 title: "Welcome to Guiders.js!"
17686 setTimeout(function() {
17687 guiders.createGuider({
17688 attachTo: "#RESSettingsButton",
17689 buttons: [{name: "Close"},
17691 description: "This is just some sorta test guider, here... woop woop.",
17694 // offset: { left: -200, top: 120 },
17696 title: "Guiders are typically attached to an element on the page."
17698 guiders.createGuider({
17699 attachTo: "a.toggleImage:first",
17700 buttons: [{name: "Close"},
17702 description: "An example of an image expando",
17705 // offset: { left: -200, top: 120 },
17707 title: "Guiders are typically attached to an element on the page."
17713 dailyTip: function() {
17714 var lastCheck = parseInt(RESStorage.getItem('RESLastToolTip'), 10) || 0;
17715 var now = new Date();
17716 // 86400000 = 1 day
17717 if ((now.getTime() - lastCheck) > 86400000) {
17718 // mark off that we've displayed a new tooltip
17719 RESStorage.setItem('RESLastToolTip',now.getTime());
17720 if (lastCheck === 0) {
17723 setTimeout(function() {
17724 modules['RESTips'].randomTip();
17729 randomTip: function() {
17730 this.currTip = Math.floor(Math.random()*this.tips.length);
17731 this.showTip(this.currTip);
17733 disableDailyTipsCheckbox: function(e) {
17734 modules['RESTips'].options.dailyTip.value = e.target.checked;
17735 RESStorage.setItem('RESoptions.RESTips', JSON.stringify(modules['RESTips'].options));
17737 nextTip: function() {
17738 if (typeof this.currTip === 'undefined') this.currTip = 0;
17739 modules['RESTips'].nextPrevTip(1);
17741 prevTip: function() {
17742 if (typeof this.currTip === 'undefined') this.currTip = 0;
17743 modules['RESTips'].nextPrevTip(-1);
17745 nextPrevTip: function(idx) {
17746 if (typeof this.currTip === 'undefined') this.currTip = 0;
17747 // if (idx<0) guiders.hideAll();
17749 this.currTip += idx;
17750 if (this.currTip < 0) {
17751 this.currTip = this.tips.length-1;
17752 } else if (this.currTip >= this.tips.length) {
17755 this.showTip(this.currTip);
17757 generateContent: function(help, elem) {
17758 var description = []
17760 if (help.message) description.push(help.message);
17762 if (help.keyboard) {
17764 // TODO: microtemplate
17765 var disabled = !modules['keyboardNav'].isEnabled();
17766 description.push('<h2 class="keyboardNav' + (disabled ? 'keyboardNavDisabled' : '') + '">');
17767 description.push('Keyboard Navigation' + (disabled ? ' (disabled)' : ''));
17768 description.push('</h2>');
17770 var keyboardTable = RESUtils.generateTable(help.keyboard, this.generateContentKeyboard, elem);
17771 if (keyboardTable) description.push(keyboardTable);
17775 description.push('<h2 class="settingsPointer">');
17776 description.push('<span class="gearIcon"></span> RES Settings');
17777 description.push('</h2>');
17779 var optionTable = RESUtils.generateTable(help.option, this.generateContentOption, elem);
17780 if (optionTable) description.push(optionTable);
17783 description = description.join("\n");
17784 return description;
17786 generateContentKeyboard: function (keyboardNavOption, index, array, elem) {
17787 var keyCode = modules['keyboardNav'].getNiceKeyCode(keyboardNavOption);
17788 if (!keyCode) return;
17790 var description = [];
17791 description.push('<tr>');
17792 description.push('<td><code>' + keyCode.toLowerCase() + '</code></td>');
17793 description.push('<td>' + keyboardNavOption + '</td>');
17794 description.push('</tr><tr>');
17795 description.push('<td> </td>'); // for styling
17796 description.push('<td>' + modules['keyboardNav'].options[keyboardNavOption].description + '</td>');
17797 description.push('</tr>');
17799 return description;
17801 generateContentOption: function (option, index, array, elem) {
17802 var module = modules[option.moduleID];
17803 if (!module) return;
17805 var description = [];
17807 description.push("<tr>");
17808 description.push("<td>" + module.category + '</td>');
17810 description.push('<td>');
17811 description.push(modules['settingsNavigation'].makeUrlHashLink(option.moduleID, null, module.moduleName));
17812 description.push('</td>');
17814 description.push('<td>');
17815 description.push(option.key
17816 ? modules['settingsNavigation'].makeUrlHashLink(option.moduleID, option.key)
17818 description.push('</td>');
17820 if (module.options[option.key]) {
17821 description.push('</tr><tr>');
17822 description.push('<td colspan="3">' + module.options[option.key].description + '</td>');
17824 description.push("</tr>");
17826 return description;
17829 message: "Roll over the gear icon <span class='gearIcon'></span> and click 'settings console' to explore the RES settings. You can enable, disable or change just about anything you like/dislike about RES!<br><br>Once you've opened the console once, this message will not appear again.",
17830 attachTo: "#openRESPrefs",
17835 message: 'Welcome to RES. You can turn modules on and off, and configure settings for the modules using the gear icon link at the top right. For feature requests, or just help getting a question answered, be sure to subscribe to <a href="http://reddit.com/r/Enhancement">/r/Enhancement</a>.',
17836 attachTo: "#openRESPrefs",
17840 message: "Click the tag icon next to a user to tag that user with any name you like - you can also color code the tag.",
17841 attachTo: ".RESUserTagImage:visible",
17843 option: { moduleID: 'userTagger' }
17846 message: "Don't forget to subscribe to <a href=\"http://reddit.com/r/Enhancement\">/r/Enhancement</a> to keep up to date on the latest versions of RES or suggest features! For bug reports, submit to <a href=\"http://reddit.com/r/RESIssues\">/r/RESIssues</a>"
17849 message: "Don't want to see posts containing certain keywords? Want to filter out certain subreddits from /r/all? Try the filteReddit module!" ,
17850 option: { moduleID: 'filteReddit' }
17853 message: "Keyboard Navigation is one of the most underutilized features in RES. You should try it!" ,
17854 option: { moduleID: 'keyboardNav' },
17855 keyboard: 'toggleHelp'
17858 message: "Did you know you can configure the appearance of a number of things in RES? For example: keyboard navigation lets you configure the look of the 'selected' box, and commentBoxes lets you configure the borders / shadows." ,
17859 option: [ { moduleID: 'keyboardNav', key: 'focusBGColor' }, { moduleID: 'styleTweaks', key: 'commentBoxes' }]
17863 message: "Do you subscribe to a ton of reddits? Give the subreddit tagger a try, it can make your homepage a bit more readable." ,
17864 option: { moduleID: 'subRedditTagger' }
17867 message: "If you haven't tried it yet, Keyboard Navigation is great. Just hit ? while browsing for instructions." ,
17868 option: { moduleID: 'keyboardNav' },
17869 keyboard: 'toggleHelp'
17872 message: "Roll over a user's name to get information about them such as their karma, and how long they've been a reddit user." ,
17873 option: { moduleID: 'userTagger', key: 'hoverInfo' }
17876 message: "Hover over the 'parent' link in comments pages to see the text of the parent being referred to." ,
17877 option: { moduleID: 'showParent' }
17880 message: "You can configure the color and style of the User Highlighter module if you want to change how the highlights look." ,
17881 option: { moduleID: 'userHighlight' }
17884 message: "Not a fan of how comments pages look? You can change the appearance in the Style Tweaks module" ,
17885 option: { moduleID: 'styleTweaks' }
17888 message: "Don't like the style in a certain subreddit? RES gives you a checkbox to disable styles individually - check the right sidebar!"
17891 message: "Looking for posts by submitter, post with photos, or posts in IAmA form? Try out the comment navigator."
17894 message: "Have you seen the <a href=\"http://www.reddit.com/r/Dashboard\">RES Dashboard</a>? It allows you to do all sorts of great stuff, like keep track of lower traffic subreddits, and manage your <a href=\"/r/Dashboard#userTaggerContents\">user tags</a> and <a href=\"/r/Dashboard#newCommentsContents\">thread subscriptions</a>!",
17895 options: { moduleID: 'dashboard' }
17898 message: "Sick of seeing these tips? They only show up once every 24 hours, but you can disable that in the RES Tips and Tricks preferences.",
17899 option: { moduleID: 'RESTips' }
17902 message: "Did you know that there is now a 'keep me logged in' option in the Account Switcher? Turn it on if you want to stay logged in to Reddit when using the switcher!",
17903 option: { moduleID: 'accountSwitcher', key: 'keepLoggedIn' }
17906 message: "See that little [vw] next to users you've voted on? That's their vote weight - it moves up and down as you vote the same user up / down.",
17907 option: { moduleID: 'userTagger', key: 'vwTooltip' }
17911 // array of guiders will go here... and we will add a "tour" button somewhere to start the tour...
17913 initTips: function() {
17914 $('body').on('click', '#disableDailyTipsCheckbox', modules['RESTips'].disableDailyTipsCheckbox);
17915 // create the special "you have never visited the console" guider...
17916 this.createGuider(0, 'console');
17917 for (var i=0, len=this.tips.length; i<len; i++) {
17918 this.createGuider(i);
17921 createGuider: function(i, special) {
17922 if (special === 'console') {
17923 var thisID = special;
17924 var thisTip = this.consoleTip;
17926 var thisID = "tip"+i;
17927 var thisTip = this.tips[i];
17929 var description = modules['RESTips'].generateContent(thisTip);
17930 var attachTo = thisTip.attachTo;
17931 var nextidx = ((parseInt(i+1, 10)) >= len) ? 0 : (parseInt(i+1, 10));
17932 var nextID = "tip"+nextidx;
17933 var thisChecked = (modules['RESTips'].options.dailyTip.value) ? 'checked="checked"' : '';
17935 attachTo: attachTo,
17938 onclick: modules['RESTips'].prevTip
17942 onclick: modules['RESTips'].nextTip
17945 description: description,
17946 buttonCustomHTML: "<label class='stopper'> <input type='checkbox' name='disableDailyTipsCheckbox' id='disableDailyTipsCheckbox' "+thisChecked+" />Show these tips once every 24 hours</label>",
17949 onShow: modules['RESTips'].onShow,
17950 onHide: modules['RESTips'].onHide,
17951 position: this.tips[i].position,
17953 title: "RES Tips and Tricks"
17955 if (special === 'console') {
17956 delete guiderObj.buttonCustomHTML;
17957 delete guiderObj.next;
17958 delete guiderObj.buttons;
17960 guiderObj.title = "RES is extremely configurable";
17964 guiders.createGuider(guiderObj);
17966 showTip: function(idx, special) {
17967 if (typeof this.tipsInitialized === 'undefined') {
17969 this.tipsInitialized = true;
17972 guiders.show('tip'+idx);
17974 guiders.show('console');
17977 onShow: function() {
17978 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'tipstricks');
17980 onHide: function() {
17981 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'tipstricks');
17986 modules['settingsNavigation'] = {
17987 moduleID: 'settingsNavigation',
17988 moduleName: 'RES Settings Navigation',
17990 description: 'Helping you get around the RES Settings Console with greater ease',
17994 isEnabled: function() {
17995 // return RESConsole.getModulePrefs(this.moduleID);
17999 /^https?:\/\/([-\w\.]+\.)?reddit\.com\/[-\w\.\/]*/i
18001 isMatchURL: function() {
18002 return RESUtils.isMatchURL(this.moduleID);
18004 beforeLoad: function() {
18005 RESUtils.addCSS('#RESSearchMenuItem { \
18009 width: 21px;height: 21px; \
18010 border: 1px #c9def2 solid; \
18011 border-radius: 3px; \
18012 background: transparent center center no-repeat; \
18013 background-image: ' + this.searchButtonIcon + '; \
18015 RESUtils.addCSS('li:hover > #RESSearchMenuItem { \
18016 border-color: #369; \
18017 background-image: ' + this.searchButtonIconHover + '; \
18021 RESUtils.addCSS(modules['settingsNavigation'].css);
18022 this.menuItem = createElementWithID('i','RESSearchMenuItem');
18023 this.menuItem.setAttribute('title', 'search settings');
18024 this.menuItem.addEventListener('click', function(e) {
18025 modules['settingsNavigation'].showSearch()
18027 RESConsole.settingsButton.appendChild(this.menuItem);
18029 if (!(this.isEnabled() && this.isMatchURL())) return;
18031 window.addEventListener('hashchange', modules['settingsNavigation'].onHashChange);
18032 window.addEventListener('popstate', modules['settingsNavigation'].onPopState);
18033 setTimeout(modules['settingsNavigation'].onHashChange, 300); // for initial pageload; wait until after RES has completed loading
18037 searchButtonIcon: "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAEJGlDQ1BJQ0MgUHJvZmlsZQAAOBGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQCqwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/hL49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjczohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrYBbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1GNjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtnE6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqwSyv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL42K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszSueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInvi0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8OokmrdtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5bRIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4bBsrEwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAONJREFUOBGlkz0KwkAQRo2ICt5IsBE9gI1dwAOk8AqCgufQPo2CYGlhIVh4Aj2ClYVg4hvdCclmI8gOPHbn78vsLgnSNK35WN2nWXq9BRoVEwyIj6ANO4ghgbLJHVjM8BM4wwGesIYm2LU1O9ClSCwCzfXZi8gkF9NcSWBB0dVRGBPbOOLOSww4qJA38d3vbanqEabEA5Mbsj4gNH42vvgFxxTIJYpd4AgvcbAVdKDQ8/lKflaz12ds4e+hBxFsIYQ7fM1W/OHPyYktIZuiagLVt9cxgRucNPGvgPZlq/e/4C3wBoAXSrzY2Qd2AAAAAElFTkSuQmCC')",
18038 searchButtonIconHover: "url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAEJGlDQ1BJQ0MgUHJvZmlsZQAAOBGFVd9v21QUPolvUqQWPyBYR4eKxa9VU1u5GxqtxgZJk6XtShal6dgqJOQ6N4mpGwfb6baqT3uBNwb8AUDZAw9IPCENBmJ72fbAtElThyqqSUh76MQPISbtBVXhu3ZiJ1PEXPX6yznfOec7517bRD1fabWaGVWIlquunc8klZOnFpSeTYrSs9RLA9Sr6U4tkcvNEi7BFffO6+EdigjL7ZHu/k72I796i9zRiSJPwG4VHX0Z+AxRzNRrtksUvwf7+Gm3BtzzHPDTNgQCqwKXfZwSeNHHJz1OIT8JjtAq6xWtCLwGPLzYZi+3YV8DGMiT4VVuG7oiZpGzrZJhcs/hL49xtzH/Dy6bdfTsXYNY+5yluWO4D4neK/ZUvok/17X0HPBLsF+vuUlhfwX4j/rSfAJ4H1H0qZJ9dN7nR19frRTeBt4Fe9FwpwtN+2p1MXscGLHR9SXrmMgjONd1ZxKzpBeA71b4tNhj6JGoyFNp4GHgwUp9qplfmnFW5oTdy7NamcwCI49kv6fN5IAHgD+0rbyoBc3SOjczohbyS1drbq6pQdqumllRC/0ymTtej8gpbbuVwpQfyw66dqEZyxZKxtHpJn+tZnpnEdrYBbueF9qQn93S7HQGGHnYP7w6L+YGHNtd1FJitqPAR+hERCNOFi1i1alKO6RQnjKUxL1GNjwlMsiEhcPLYTEiT9ISbN15OY/jx4SMshe9LaJRpTvHr3C/ybFYP1PZAfwfYrPsMBtnE6SwN9ib7AhLwTrBDgUKcm06FSrTfSj187xPdVQWOk5Q8vxAfSiIUc7Z7xr6zY/+hpqwSyv0I0/QMTRb7RMgBxNodTfSPqdraz/sDjzKBrv4zu2+a2t0/HHzjd2Lbcc2sG7GtsL42K+xLfxtUgI7YHqKlqHK8HbCCXgjHT1cAdMlDetv4FnQ2lLasaOl6vmB0CMmwT/IPszSueHQqv6i/qluqF+oF9TfO2qEGTumJH0qfSv9KH0nfS/9TIp0Wboi/SRdlb6RLgU5u++9nyXYe69fYRPdil1o1WufNSdTTsp75BfllPy8/LI8G7AUuV8ek6fkvfDsCfbNDP0dvRh0CrNqTbV7LfEEGDQPJQadBtfGVMWEq3QWWdufk6ZSNsjG2PQjp3ZcnOWWing6noonSInvi0/Ex+IzAreevPhe+CawpgP1/pMTMDo64G0sTCXIM+KdOnFWRfQKdJvQzV1+Bt8OokmrdtY2yhVX2a+qrykJfMq4Ml3VR4cVzTQVz+UoNne4vcKLoyS+gyKO6EHe+75Fdt0Mbe5bRIf/wjvrVmhbqBN97RD1vxrahvBOfOYzoosH9bq94uejSOQGkVM6sN/7HelL4t10t9F4gPdVzydEOx83Gv+uNxo7XyL/FtFl8z9ZAHF4bBsrEwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAPBJREFUOBGlkTEKAjEQRTciIth4HkvRA9jYCR7AwisICvZiZ6X9NlvYW1gIFp5AT2Eh6PpGMjLuBkUy8JmZ5P+fZOLyPE9iohIjFm20QTV0A+dch/UeqIMtSHnqg1wOmYEFjAkQ8hHswA1sQM3ytC6KWxBlqqM3IUna9GIy1DWbiwYziGdLkJpIQVZclz40REbgnKhMSB/+b+sKSZ8wpnb+9C71FQwsV+uPJ3iBDFFOO4E9uPt+TW6oUHPJwJvINy7BCvTBAohpBpoqfnFt861GOPUmc8vTd7L3O5it3OaCwUHZfxmoyObQN9r9n3W0wRPmWv0jZnGemgAAAABJRU5ErkJggg==')",
18039 consoleTip: function() {
18040 // first, ensure that we've at least run dailyTip once (so RES first-run has happened)...
18041 var lastToolTip = RESStorage.getItem('RESLastToolTip');
18043 // if yes, see if the user has ever opened the settings console.
18044 var hasOpenedConsole = RESStorage.getItem('RESConsole.hasOpenedConsole');
18045 if (!hasOpenedConsole) {
18046 // if no, nag them once daily that the console exists until they use it. Once it's been opened, this check will never run again.
18047 var lastCheckDailyTip = parseInt(RESStorage.getItem('RESLastToolTip'), 10) || 0;
18048 var now = new Date();
18049 // 86400000 = 1 day - remind users once a day if they've never opened the settings that they should check out the console sometime...
18050 var lastCheck = parseInt(RESStorage.getItem('RESConsole.hasOpenedCheck'), 10) || 0;
18051 if (((now.getTime() - lastCheckDailyTip) > 1000) && ((now.getTime() - lastCheck) > 86400000)) {
18052 RESStorage.setItem('RESConsole.hasOpenedCheck', now.getTime());
18053 modules['RESTips'].showTip(0,'console');
18058 makeUrlHashLink: function (moduleID, optionKey, displayText, cssClass) {
18059 if (!displayText) {
18061 displayText = optionKey;
18062 } else if (modules[moduleID]) {
18063 displayText = modules[moduleID].moduleName;
18064 } else if (moduleID) {
18065 displayText = moduleID;
18067 displayText = 'Settings';
18071 var hash = modules['settingsNavigation'].makeUrlHash(moduleID, optionKey);
18072 var link = ['<a ', 'class="', cssClass || '', '" ', 'href="', hash, '"', '>', displayText, '</a>'].join('');
18075 makeUrlHash: function(moduleID, optionKey) {
18076 var hashComponents = ['#!settings']
18079 hashComponents.push(moduleID);
18082 if (moduleID && optionKey) {
18083 hashComponents.push(optionKey);
18086 var hash = hashComponents.join('/');
18089 setUrlHash: function(moduleID, optionKey) {
18090 var titleComponents = ['RES Settings'];
18093 var module = modules[moduleID];
18094 var moduleName = module && module.moduleName || moduleID;
18095 titleComponents.push(moduleName);
18098 titleComponents.push(optionKey);
18102 var hash = this.makeUrlHash(moduleID, optionKey);
18103 var title = titleComponents.join(' - ');
18105 if (window.location.hash != hash) {
18106 window.history.pushState(hash, title, hash);
18109 resetUrlHash: function() {
18110 window.location.hash = "";
18112 onHashChange: function (event) {
18113 var hash = window.location.hash;
18114 if (hash.substring(0, 10) !== '#!settings') return;
18116 var params = hash.match(/\/[\w\s]+/g);
18117 if (params && params[0]) {
18118 var moduleID = params[0].substring(1);
18120 if (params && params[1]) {
18121 var optionKey = params[1].substring(1);
18124 modules['settingsNavigation'].loadSettingsPage(moduleID, optionKey);
18126 onPopState: function (event) {
18127 var state = typeof event.state === "string" && event.state.split('/');
18128 if (!state || state[0] !== '#!settings') {
18129 if (RESConsole.isOpen) {
18130 RESConsole.close();
18135 var moduleID = state[1];
18136 var optionKey = state[2];
18138 modules['settingsNavigation'].loadSettingsPage(moduleID, optionKey);
18140 loadSettingsPage: function(moduleID, optionKey, optionValue) {
18141 if (moduleID && modules.hasOwnProperty(moduleID)) {
18142 var module = modules[moduleID];
18145 var category = module.category;
18149 RESConsole.open(module && module.moduleID);
18151 if (optionKey && module.options.hasOwnProperty(optionKey)) {
18152 var optionsPanel = $(RESConsole.RESConsoleContent);
18153 var optionElement = optionsPanel.find('label[for="' + optionKey + '"]');
18154 var optionParent = optionElement.parent();
18155 optionParent.addClass('highlight');
18156 if (optionElement.length) {
18157 var configPanel = $(RESConsole.RESConsoleConfigPanel);
18158 var offset = optionElement.offset().top - configPanel.offset().top;
18159 optionsPanel.scrollTop(offset);
18165 this.search(optionKey);
18172 search: function(query) {
18173 RESConsole.openCategoryPanel('About RES');
18174 modules['settingsNavigation'].drawSearchResults(query);
18175 modules['settingsNavigation'].getSearchResults(query);
18176 modules['settingsNavigation'].setUrlHash('search', query);
18178 showSearch: function () {
18179 RESConsole.hidePrefsDropdown();
18180 modules['settingsNavigation'].drawSearchResults();
18181 $('#SearchRES-input').focus();
18183 doneSearch: function (query, results) {
18184 modules['settingsNavigation'].drawSearchResults(query, results);
18186 getSearchResults: function (query) {
18187 if (!(query && query.toString().length)) {
18188 modules['settingsNavigation'].doneSearch(query, []);
18191 var queryTerms = modules['settingsNavigation'].prepareSearchText(query, true).split(' ');
18195 for (var moduleKey in modules) {
18196 if (!modules.hasOwnProperty(moduleKey)) continue;
18197 var module = modules[moduleKey];
18200 var searchString = module.moduleID + module.moduleName + module.category + module.description;
18201 searchString = modules['settingsNavigation'].prepareSearchText(searchString, false);
18202 var matches = modules['settingsNavigation'].searchMatches(queryTerms, searchString);
18204 var result = modules['settingsNavigation'].makeModuleSearchResult(moduleKey);
18205 result.rank = matches;
18206 results.push(result);
18210 var options = module.options;
18212 for (var optionKey in options) {
18213 if (!options.hasOwnProperty(optionKey)) continue;
18214 var option = options[optionKey];
18216 var searchString = module.moduleID + module.moduleName + module.category + optionKey + option.description;
18217 searchString = modules['settingsNavigation'].prepareSearchText(searchString, false);
18218 var matches = modules['settingsNavigation'].searchMatches(queryTerms, searchString);
18220 var result = modules['settingsNavigation'].makeOptionSearchResult(moduleKey, optionKey);
18221 result.rank = matches;
18222 results.push(result);
18227 results.sort(function(a, b) {
18228 var comparison = b.rank - a.rank;
18231 if (comparison === 0) {
18233 a.title < b.title ? -1
18234 : a.title > b.title ? 1
18239 if (comparison === 0) {
18241 a.description < b.description ? -1
18242 : a.description > b.description ? 1
18250 modules['settingsNavigation'].doneSearch(query, results);
18253 searchMatches: function(needles, haystack) {
18254 if (!(haystack && haystack.length))
18257 var numMatches = 0;
18258 for (var i = 0; i < needles.length; i++) {
18259 if (haystack.indexOf(needles[i]) !== -1)
18265 prepareSearchText: function (text, preserveSpaces) {
18266 if (typeof text === "undefined" || text === null) {
18270 var replaceSpacesWith = !!preserveSpaces ? ' ' : ''
18271 return text.toString().toLowerCase()
18272 .replace(/[,\/]/g,replaceSpacesWith).replace(/\s+/g, replaceSpacesWith);
18274 makeOptionSearchResult: function (moduleKey, optionKey) {
18275 var module = modules[moduleKey];
18276 var option = module.options[optionKey];
18279 result.type = 'option';
18280 result.breadcrumb = ['Settings',
18282 module.moduleName + ' (' + module.moduleID + ')'
18284 result.title = optionKey;
18285 result.description = option.description;
18286 result.moduleID = moduleKey;
18287 result.optionKey = optionKey;
18291 makeModuleSearchResult: function (moduleKey) {
18292 var module = modules[moduleKey];
18295 result.type = 'module';
18296 result.breadcrumb = ['Settings',
18298 '(' + module.moduleID + ')'
18300 result.title = module.moduleName;
18301 result.description = module.description;
18302 result.moduleID = moduleKey;
18307 onSearchResultSelected: function(result) {
18308 if (!result) return;
18310 switch (result.type) {
18312 modules['settingsNavigation'].loadSettingsPage(result.moduleID);
18315 modules['settingsNavigation'].loadSettingsPage(result.moduleID, result.optionKey);
18318 alert('Could not load search result');
18322 // ---------- View ------
18324 #SearchRES #SearchRES-results-container { \
18327 #SearchRES #SearchRES-results-container + #SearchRES-boilerplate { margin-top: 1em; border-top: 1px black solid; padding-top: 1em; } \
18329 margin-top: 1.5em; \
18331 #SearchRES-results { \
18333 #SearchRES-results li { \
18334 list-style-type: none; \
18335 border-bottom: 1px dashed #ccc; \
18337 margin-left: 0px; \
18338 padding-left: 10px; \
18339 padding-top: 24px; \
18340 padding-bottom: 24px; \
18342 #SearchRES-results li:hover { \
18343 background-color: #FAFAFF; \
18345 .SearchRES-result-title { \
18346 margin-bottom: 12px; \
18347 font-weight: bold; \
18350 .SearchRES-breadcrumb { \
18351 font-weight: normal; \
18354 .SearchRES-result-copybutton {\
18360 background: no-repeat center center; \
18361 background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABoAAAAWCAYAAADeiIy1AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA+5pVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1wTU06T3JpZ2luYWxEb2N1bWVudElEPSJ1dWlkOjY1RTYzOTA2ODZDRjExREJBNkUyRDg4N0NFQUNCNDA3IiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOkUwRjM3M0QxMDY5NTExRTI5OUZEQTZGODg4RDc1ODdCIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOkUwRjM3M0QwMDY5NTExRTI5OUZEQTZGODg4RDc1ODdCIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDUzYgKE1hY2ludG9zaCkiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDowMTgwMTE3NDA3MjA2ODExODA4M0ZFMkJBM0M1RUU2NSIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDowNjgwMTE3NDA3MjA2ODExODA4M0U3NkRBMDNEMDVDMSIvPiA8ZGM6dGl0bGU+IDxyZGY6QWx0PiA8cmRmOmxpIHhtbDpsYW5nPSJ4LWRlZmF1bHQiPmdseXBoaWNvbnM8L3JkZjpsaT4gPC9yZGY6QWx0PiA8L2RjOnRpdGxlPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/Pn00ay4AAAEISURBVHjavJWBDYQgDEXhcgMwwm2gIzgCIzjCjeAIjsAIN8KN4Ai6wW3AtTm4EChYkdjkm2javLa2IKy1olZgL9CD5XsShI8PSF8B8pqvAqEWUJ8FYemglQhEmfg/gA0uhvLHVo5EUtmAf3ZgCoNB74xvLkEVgnwlJtOep8vSVihM9veRAKiDFdhCK/VdECal9JBONLSkIhzVBpWUW4cT1giSDEMMmqP+GjdxA2OPiuMdgxb3bQozOr2wBMhSGTFAppQYBZoqDtWR4ZuA1MFromf60gt7AKZyp0oo6UD4ImuWEJYbB6Dbi28BYsXfQJsLMBUQH7Nx/HWDU4B3le9cfCWtHAjqK8AAypyhqqqagq4AAAAASUVORK5CYII="); \
18364 #SearchRES-results li:hover .SearchRES-result-copybutton { display: block; } \
18365 #SearchRES-input-submit { \
18366 margin-left: 8px; \
18368 #SearchRES-input { \
18373 #SearchRES-input-container { \
18375 margin-left: 3em; \
18379 searchPanelHtml: '\
18380 <h3>Search RES Settings Console</h3> \
18381 <div id="SearchRES-results-container"> \
18382 <h4>Results for: <span id="SearchRES-query"></span></h4> \
18383 <ul id="SearchRES-results"></ul> \
18384 <p id="SearchRES-results-none">No results found</p> \
18386 <div id="SearchRES-boilerplate">\
18387 <p>You can search for RES options by module name, option name, and description. For example, try searching for "daily trick" in one of the following ways:</p>\
18389 <li>type <code>daily trick</code> in the search box above and click the button</li> \
18390 <li>press <code>.</code> to open the RES console, type in <code>search <em>daily trick</em></code>, and press Enter</li> \
18394 renderSearchPanel: function() {
18395 var searchPanel = $('<div />').html(modules['settingsNavigation'].searchPanelHtml);
18396 searchPanel.delegate('#SearchRES-results-container .SearchRES-result-item', 'click', modules['settingsNavigation'].handleSearchResultClick);
18398 modules['settingsNavigation'].searchPanel = searchPanel;
18399 return searchPanel;
18402 renderSearchForm: function() {
18403 var RESSearchContainer = createElementWithID('form', 'SearchRES-input-container');
18405 var RESSearchBox = createElementWithID('input', 'SearchRES-input');
18406 RESSearchBox.setAttribute('type', 'text');
18407 RESSearchBox.setAttribute('placeholder', 'search RES settings');
18409 var RESSearchButton = createElementWithID('input', 'SearchRES-input-submit');
18410 RESSearchButton.classList.add('blueButton');
18411 RESSearchButton.setAttribute('type', 'submit');
18412 RESSearchButton.setAttribute('value', 'search');
18414 RESSearchContainer.appendChild(RESSearchBox);
18415 RESSearchContainer.appendChild(RESSearchButton);
18417 RESSearchContainer.addEventListener('submit', function (e) {
18418 e.preventDefault();
18419 modules['settingsNavigation'].search(RESSearchBox.value);
18424 searchForm = RESSearchContainer;
18425 return RESSearchContainer;
18427 drawSearchResultsPage: function() {
18428 if (!RESConsole.isOpen) {
18432 if (!$('#SearchRES').is(':visible')) {
18433 RESConsole.openCategoryPanel('About RES');
18435 // Open "Search RES" page
18436 $('#Button-SearchRES', this.RESConsoleContent).trigger('click', { duration: 0 });
18439 drawSearchResults: function (query, results) {
18440 modules['settingsNavigation'].drawSearchResultsPage();
18442 var resultsContainer = $('#SearchRES-results-container', modules['settingsNavigation'].searchPanel);
18444 if (!(query && query.length)) {
18445 resultsContainer.hide();
18449 resultsContainer.show();
18450 resultsContainer.find('#SearchRES-query').text(query);
18451 $("#SearchRES-input", modules['settingsNavigation'].searchForm).val(query);
18453 if (!(results && results.length)) {
18454 resultsContainer.find('#SearchRES-results-none').show();
18455 resultsContainer.find('#SearchRES-results').hide();
18457 resultsContainer.find('#SearchRES-results-none').hide();
18458 var resultsList = $('#SearchRES-results', resultsContainer).show();
18460 resultsList.empty();
18461 for (var i = 0; i < results.length; i++) {
18462 var result = results[i];
18464 var element = modules['settingsNavigation'].drawSearchResultItem(result);
18465 resultsList.append(element);
18469 drawSearchResultItem: function(result) {
18470 var element = $('<li>');
18471 element.addClass('SearchRES-result-item')
18472 .data('SearchRES-result', result);
18474 $('<span>', { class: 'SearchRES-result-copybutton'})
18476 .attr('title', 'copy this for a comment')
18478 var breadcrumb = $('<span>', {class: 'SearchRES-breadcrumb'})
18479 .text(result.breadcrumb + ' > ');
18480 $('<div>', {class: 'SearchRES-result-title'})
18481 .append(breadcrumb)
18482 .append(result.title)
18483 .appendTo(element);
18484 $('<div>', {class: 'SearchRES-result-description'})
18486 .html(result.description);
18490 handleSearchResultClick: function (e) {
18491 var element = $(this);
18492 var result = element.data('SearchRES-result');
18493 if ($(e.target).is('.SearchRES-result-copybutton')) {
18494 modules['settingsNavigation'].onSearchResultCopy(result, element);
18496 modules['settingsNavigation'].onSearchResultSelected(result);
18498 e.preventDefault();
18500 onSearchResultCopy: function(result, element) {
18501 var markdown = modules['settingsNavigation'].makeOptionSearchResultLink(result);
18502 alert('<textarea rows="5" cols="50">' + markdown + '</textarea>');
18504 makeOptionSearchResultLink: function (result) {
18505 var url = document.location.pathname +
18506 modules['settingsNavigation'].makeUrlHash(result.moduleID, result.optionKey);
18510 '[' + result.title + '](' + url + ')',
18512 result.description,
18523 modules['dashboard'] = {
18524 moduleID: 'dashboard',
18525 moduleName: 'RES Dashboard',
18531 description: 'Number of posts to show by default in each widget'
18536 { name: 'hot', value: 'hot' },
18537 { name: 'new', value: 'new' },
18538 { name: 'controversial', value: 'controversial' },
18539 { name: 'top', value: 'top' }
18542 description: 'Default sort method for new widgets'
18544 dashboardShortcut: {
18547 description: 'Show +dashboard shortcut in sidebar for easy addition of dashboard widgets.'
18552 description: 'How many user tags to show per page. (enter zero to show all on one page)'
18555 description: 'The RES Dashboard is home to a number of features including widgets and other useful tools',
18556 isEnabled: function() {
18557 return RESConsole.getModulePrefs(this.moduleID);
18560 /^https?:\/\/([-\w\.]+\.)?reddit\.com\/[-\w\.\/]*/i
18562 isMatchURL: function() {
18563 return RESUtils.isMatchURL(this.moduleID);
18566 if (this.isEnabled()) {
18567 this.getLatestWidgets();
18568 RESUtils.addCSS('.RESDashboardToggle { margin-right: 5px; color: white; background-image: url(http://www.redditstatic.com/bg-button-add.png); cursor: pointer; text-align: center; width: 68px; font-weight: bold; font-size: 10px; border: 1px solid #444; padding: 1px 6px; border-radius: 3px 3px 3px 3px; }');
18569 RESUtils.addCSS('.RESDashboardToggle.remove { background-image: url(http://www.redditstatic.com/bg-button-remove.png) }');
18570 if (this.isMatchURL()) {
18571 $('#RESDropdownOptions').prepend('<li id="DashboardLink"><a href="/r/Dashboard">my dashboard</a></li>');
18572 if (RESUtils.currentSubreddit()) {
18573 RESUtils.addCSS('.RESDashboardToggle {}');
18574 // one more safety check... not sure how people's widgets[] arrays are breaking.
18575 if (!(this.widgets instanceof Array)) {
18578 if (RESUtils.currentSubreddit('dashboard')) {
18579 $('#noresults, #header-bottom-left .tabmenu:not(".viewimages")').hide();
18580 $('#header-bottom-left .redditname a:first').text('My Dashboard');
18581 this.drawDashboard();
18583 if (this.options.dashboardShortcut.value == true) this.addDashboardShortcuts();
18588 getLatestWidgets: function() {
18590 this.widgets = JSON.parse(RESStorage.getItem('RESmodules.dashboard.' + RESUtils.loggedInUser())) || [];
18595 loader: 'data:image/gif;base64,R0lGODlhEAAQAPQAAP///2+NyPb3+7zK5e3w95as1rPD4W+NyKC02oOdz8/Z7Nnh8HqVzMbS6XGOyI2l06m73gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh/hpDcmVhdGVkIHdpdGggYWpheGxvYWQuaW5mbwAh+QQJCgAAACwAAAAAEAAQAAAFdyAgAgIJIeWoAkRCCMdBkKtIHIngyMKsErPBYbADpkSCwhDmQCBethRB6Vj4kFCkQPG4IlWDgrNRIwnO4UKBXDufzQvDMaoSDBgFb886MiQadgNABAokfCwzBA8LCg0Egl8jAggGAA1kBIA1BAYzlyILczULC2UhACH5BAkKAAAALAAAAAAQABAAAAV2ICACAmlAZTmOREEIyUEQjLKKxPHADhEvqxlgcGgkGI1DYSVAIAWMx+lwSKkICJ0QsHi9RgKBwnVTiRQQgwF4I4UFDQQEwi6/3YSGWRRmjhEETAJfIgMFCnAKM0KDV4EEEAQLiF18TAYNXDaSe3x6mjidN1s3IQAh+QQJCgAAACwAAAAAEAAQAAAFeCAgAgLZDGU5jgRECEUiCI+yioSDwDJyLKsXoHFQxBSHAoAAFBhqtMJg8DgQBgfrEsJAEAg4YhZIEiwgKtHiMBgtpg3wbUZXGO7kOb1MUKRFMysCChAoggJCIg0GC2aNe4gqQldfL4l/Ag1AXySJgn5LcoE3QXI3IQAh+QQJCgAAACwAAAAAEAAQAAAFdiAgAgLZNGU5joQhCEjxIssqEo8bC9BRjy9Ag7GILQ4QEoE0gBAEBcOpcBA0DoxSK/e8LRIHn+i1cK0IyKdg0VAoljYIg+GgnRrwVS/8IAkICyosBIQpBAMoKy9dImxPhS+GKkFrkX+TigtLlIyKXUF+NjagNiEAIfkECQoAAAAsAAAAABAAEAAABWwgIAICaRhlOY4EIgjH8R7LKhKHGwsMvb4AAy3WODBIBBKCsYA9TjuhDNDKEVSERezQEL0WrhXucRUQGuik7bFlngzqVW9LMl9XWvLdjFaJtDFqZ1cEZUB0dUgvL3dgP4WJZn4jkomWNpSTIyEAIfkECQoAAAAsAAAAABAAEAAABX4gIAICuSxlOY6CIgiD8RrEKgqGOwxwUrMlAoSwIzAGpJpgoSDAGifDY5kopBYDlEpAQBwevxfBtRIUGi8xwWkDNBCIwmC9Vq0aiQQDQuK+VgQPDXV9hCJjBwcFYU5pLwwHXQcMKSmNLQcIAExlbH8JBwttaX0ABAcNbWVbKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICSRBlOY7CIghN8zbEKsKoIjdFzZaEgUBHKChMJtRwcWpAWoWnifm6ESAMhO8lQK0EEAV3rFopIBCEcGwDKAqPh4HUrY4ICHH1dSoTFgcHUiZjBhAJB2AHDykpKAwHAwdzf19KkASIPl9cDgcnDkdtNwiMJCshACH5BAkKAAAALAAAAAAQABAAAAV3ICACAkkQZTmOAiosiyAoxCq+KPxCNVsSMRgBsiClWrLTSWFoIQZHl6pleBh6suxKMIhlvzbAwkBWfFWrBQTxNLq2RG2yhSUkDs2b63AYDAoJXAcFRwADeAkJDX0AQCsEfAQMDAIPBz0rCgcxky0JRWE1AmwpKyEAIfkECQoAAAAsAAAAABAAEAAABXkgIAICKZzkqJ4nQZxLqZKv4NqNLKK2/Q4Ek4lFXChsg5ypJjs1II3gEDUSRInEGYAw6B6zM4JhrDAtEosVkLUtHA7RHaHAGJQEjsODcEg0FBAFVgkQJQ1pAwcDDw8KcFtSInwJAowCCA6RIwqZAgkPNgVpWndjdyohACH5BAkKAAAALAAAAAAQABAAAAV5ICACAimc5KieLEuUKvm2xAKLqDCfC2GaO9eL0LABWTiBYmA06W6kHgvCqEJiAIJiu3gcvgUsscHUERm+kaCxyxa+zRPk0SgJEgfIvbAdIAQLCAYlCj4DBw0IBQsMCjIqBAcPAooCBg9pKgsJLwUFOhCZKyQDA3YqIQAh+QQJCgAAACwAAAAAEAAQAAAFdSAgAgIpnOSonmxbqiThCrJKEHFbo8JxDDOZYFFb+A41E4H4OhkOipXwBElYITDAckFEOBgMQ3arkMkUBdxIUGZpEb7kaQBRlASPg0FQQHAbEEMGDSVEAA1QBhAED1E0NgwFAooCDWljaQIQCE5qMHcNhCkjIQAh+QQJCgAAACwAAAAAEAAQAAAFeSAgAgIpnOSoLgxxvqgKLEcCC65KEAByKK8cSpA4DAiHQ/DkKhGKh4ZCtCyZGo6F6iYYPAqFgYy02xkSaLEMV34tELyRYNEsCQyHlvWkGCzsPgMCEAY7Cg04Uk48LAsDhRA8MVQPEF0GAgqYYwSRlycNcWskCkApIyEAOwAAAAAAAAAAAA==',
18596 drawDashboard: function() {
18597 // this first line hides the "you need RES 4.0+ to view the dashboard" link
18598 RESUtils.addCSS('.id-t3_qi5iy {display: none;}');
18599 RESUtils.addCSS('.RESDashboardComponent { position: relative; border: 1px solid #ccc; border-radius: 3px 3px 3px 3px; overflow: hidden; margin-bottom: 10px; }');
18600 RESUtils.addCSS('.RESDashboardComponentHeader { box-sizing: border-box; padding: 5px 0 8px 0; background-color: #f0f3fc; overflow: hidden; }');
18601 RESUtils.addCSS('.RESDashboardComponentScrim { position: absolute; top: 0; bottom: 0; left: 0; right: 0; z-index: 5; display: none; }');
18602 RESUtils.addCSS('.RESDashboardComponentLoader { box-sizing: border-box; position: absolute; background-color: #f2f9ff; border: 1px solid #b9d7f4; border-radius: 3px 3px 3px 3px; width: 314px; height: 40px; left: 50%; top: 50%; margin-left: -167px; margin-top: -20px; text-align: center; padding-top: 11px; }');
18603 RESUtils.addCSS('.RESDashboardComponentLoader span { position: relative; top: -6px; left: 5px; } ');
18604 RESUtils.addCSS('.RESDashboardComponentContainer { padding: 10px 15px 0 15px; min-height: 100px; }');
18605 RESUtils.addCSS('.RESDashboardComponentContainer.minimized { display: none; }');
18606 RESUtils.addCSS('.RESDashboardComponent a.widgetPath, .addNewWidget, .editWidget { display: inline-block; margin-left: 0; margin-top: 7px; color: #000; font-weight: bold; }');
18607 RESUtils.addCSS('.editWidget { float: left; margin-right: 10px; } ');
18608 RESUtils.addCSS('.RESDashboardComponent a.widgetPath { margin-left: 15px; vertical-align: top; width: 120px; overflow: hidden; text-overflow: ellipsis; }');
18609 RESUtils.addCSS('#RESDashboardAddComponent, #RESDashboardEditComponent { box-sizing: border-box; padding: 5px 8px 5px 8px; vertical-align: middle; background-color: #cee3f8; border: 1px solid #369;}');
18610 RESUtils.addCSS('#RESDashboardEditComponent { display: none; position: absolute; }');
18611 // RESUtils.addCSS('#RESDashboardComponentScrim, #RESDashboardComponentLoader { background-color: #ccc; opacity: 0.3; border: 1px solid red; display: none; }');
18612 RESUtils.addCSS('#addRedditFormContainer, #addMailWidgetContainer, #addUserFormContainer { display: none; }');
18613 RESUtils.addCSS('#addWidgetButtons, #addRedditFormContainer, #addMailWidgetContainer, #addUserFormContainer, #editRedditFormContainer { width: auto; min-width: 550px; height: 28px; float: right; text-align: right; }');
18614 RESUtils.addCSS('#editRedditFormContainer { width: auto; }');
18615 RESUtils.addCSS('#addUserForm, #addRedditForm { display: inline-block }');
18616 RESUtils.addCSS('#addUser { width: 200px; height: 24px; }');
18617 RESUtils.addCSS('#addRedditFormContainer ul.token-input-list-facebook, #editRedditFormContainer ul.token-input-list-facebook { float: left; }');
18618 RESUtils.addCSS('#addReddit { width: 115px; background-color: #fff; border: 1px solid #96bfe8; margin-left: 6px; margin-right: 6px; padding: 1px 2px 1px 2px; }');
18619 RESUtils.addCSS('#addRedditDisplayName, #editRedditDisplayName { width: 140px; height: 24px; background-color: #fff; border: 1px solid #96bfe8; margin-left: 6px; margin-right: 6px; padding: 1px 2px 1px 2px; }');
18620 RESUtils.addCSS('#editReddit { width: 5px; } ');
18621 RESUtils.addCSS('.addButton, .updateButton { cursor: pointer; display: inline-block; width: auto; padding: 3px 5px; font-size: 11px; color: #fff; border: 1px solid #636363; border-radius: 3px; background-color: #5cc410; margin-top: 3px; margin-left: 5px; }');
18622 RESUtils.addCSS('.cancelButton { width: 50px; text-align: center; cursor: pointer; display: inline-block; padding: 3px 5px; font-size: 11px; color: #fff; border: 1px solid #636363; border-radius: 3px; background-color: #D02020; margin-top: 3px; margin-left: 5px; }');
18623 RESUtils.addCSS('.backToWidgetTypes { display: inline-block; vertical-align: top; margin-top: 8px; font-weight: bold; color: #000; cursor: pointer; }');
18624 RESUtils.addCSS('.RESDashboardComponentHeader ul { font-family: Verdana; font-size: 13px; box-sizing: border-box; line-height: 22px; display: inline-block; margin-top: 2px; }');
18625 RESUtils.addCSS('.RESDashboardComponentHeader ul li { box-sizing: border-box; vertical-align: middle; height: 24px; display: inline-block; cursor: pointer; padding: 0 6px; border: 1px solid #c7c7c7; background-color: #fff; color: #6c6c6c; border-radius: 3px 3px 3px 3px; }');
18626 RESUtils.addCSS('.RESDashboardComponentHeader .editButton { display: inline-block; padding: 0; width: 24px; -moz-box-sizing: border-box; vertical-align: middle; margin-left: 10px; } ');
18627 RESUtils.addCSS('.RESDashboardComponent.minimized ul li { display: none; }');
18628 RESUtils.addCSS('.RESDashboardComponent.minimized li.RESClose, .RESDashboardComponent.minimized li.minimize { display: inline-block; }');
18629 RESUtils.addCSS('ul.widgetSortButtons li { margin-right: 10px; }');
18630 RESUtils.addCSS('.RESDashboardComponentHeader ul li.active, .RESDashboardComponentHeader ul li:hover { background-color: #a6ccf1; color: #fff; border-color: #699dcf; }');
18631 RESUtils.addCSS('ul.widgetStateButtons li { margin-right: 5px; }');
18632 RESUtils.addCSS('ul.widgetStateButtons li:last-child { margin-right: 0; }');
18633 RESUtils.addCSS('ul.widgetStateButtons li.disabled { background-color: #ddd; }');
18634 RESUtils.addCSS('ul.widgetStateButtons li.disabled:hover { cursor: auto; background-color: #ddd; color: #6c6c6c; border: 1px solid #c7c7c7; }');
18635 RESUtils.addCSS('ul.widgetSortButtons { margin-left: 10px; }');
18636 RESUtils.addCSS('ul.widgetStateButtons { float: right; margin-right: 8px; }');
18637 RESUtils.addCSS('ul.widgetStateButtons li.updateTime { cursor: auto; background: none; border: none; color: #afafaf; font-size: 9px; padding-right: 0; }');
18638 RESUtils.addCSS('ul.widgetStateButtons li.minimize, ul.widgetStateButtons li.close { font-size: 24px; }');
18639 RESUtils.addCSS('.minimized ul.widgetStateButtons li.minimize { font-size: 14px; }');
18640 RESUtils.addCSS('ul.widgetStateButtons li.refresh { margin-left: 3px; width: 24px; position:relative; padding: 0; }');
18641 RESUtils.addCSS('ul.widgetStateButtons li.refresh div { height: 16px; width: 16px; position: absolute; left: 4px; top: 4px; background-image: url(\'http://e.thumbs.redditmedia.com/r22WT2K4sio9Bvev.png\'); background-repeat: no-repeat; background-position: -16px -209px; }');
18642 RESUtils.addCSS('#userTaggerContents .show { display: inline-block; }');
18643 RESUtils.addCSS('#tagPageControls { display: inline-block; position: relative; top: 9px;}');
18645 var dbLinks = $('span.redditname a');
18646 if ($(dbLinks).length > 1) {
18647 $(dbLinks[0]).addClass('active');
18650 // add each subreddit widget...
18651 // add the "add widget" form...
18652 this.attachContainer();
18653 this.attachAddComponent();
18654 this.attachEditComponent();
18655 this.initUpdateQueue();
18657 initUpdateQueue: function() {
18658 modules['dashboard'].updateQueue = [];
18659 for (var i in this.widgets) if (this.widgets[i]) this.addWidget(this.widgets[i]);
18660 setTimeout(function () {
18661 $('#RESDashboard').dragsort({
18662 dragSelector: "div.RESDashboardComponentHeader",
18663 dragSelectorExclude: 'a, li, li.refreshAll, li.refresh > div, .editButton',
18664 dragEnd: modules['dashboard'].saveOrder,
18665 placeHolderTemplate: "<div class='placeHolder'><div></div></div>"
18669 addToUpdateQueue: function(updateFunction) {
18670 modules['dashboard'].updateQueue.push(updateFunction);
18671 if (!modules['dashboard'].updateQueueTimer) {
18672 modules['dashboard'].updateQueueTimer = setInterval(modules['dashboard'].processUpdateQueue, 2000);
18673 setTimeout(modules['dashboard'].processUpdateQueue, 100);
18676 processUpdateQueue: function() {
18677 var thisUpdate = modules['dashboard'].updateQueue.pop();
18679 if (modules['dashboard'].updateQueue.length < 1) {
18680 clearInterval(modules['dashboard'].updateQueueTimer);
18681 delete modules['dashboard'].updateQueueTimer;
18684 saveOrder: function() {
18685 var data = $("#siteTable li.RESDashboardComponent").map(function() { return $(this).attr("id"); }).get();
18688 for (var i=0, len=modules['dashboard'].widgets.length; i<len; i++) {
18689 var newIndex = data.indexOf(modules['dashboard'].widgets[i].basePath.replace(/(\/|\+)/g, '_'));
18690 newOrder[newIndex] = modules['dashboard'].widgets[i];
18692 modules['dashboard'].widgets = newOrder;
18694 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
18696 attachContainer: function() {
18697 this.siteTable = $('#siteTable.linklisting');
18698 $(this.siteTable).append('<div id="dashboardContents" class="dashboardPane" />');
18699 if ((location.hash !== '') && (location.hash !== '#dashboardContents')) {
18700 $('span.redditname a').removeClass('active');
18701 var activeTabID = location.hash.replace('#','#tab-');
18702 $(activeTabID).addClass('active');
18703 $('.dashboardPane').hide();
18704 $(location.hash).show();
18706 $('#userTaggerContents').hide();
18708 $('span.redditname a:first').click(function(e) {
18709 e.preventDefault();
18710 location.hash = 'dashboardContents';
18711 $('span.redditname a').removeClass('active');
18712 $(this).addClass('active');
18713 $('.dashboardPane').hide();
18714 $('#dashboardContents').show();
18717 attachEditComponent: function() {
18718 this.dashboardContents = $('#dashboardContents');
18719 this.dashboardEditComponent = $('<div id="RESDashboardEditComponent" class="RESDashboardComponent" />');
18720 $(this.dashboardEditComponent).html(' \
18721 <div class="editWidget">Edit widget</div> \
18722 <div id="editRedditFormContainer" class="editRedditForm"> \
18723 <form id="editRedditForm"><input type="text" id="editReddit"><input type="text" id="editRedditDisplayName" placeholder="display name (e.g. stuff)"><input type="submit" class="updateButton" value="save changes"> <input type="cancel" class="cancelButton" value="cancel"></form> \
18726 var thisEle = $(this.dashboardEditComponent).find('#editReddit');
18728 $(this.dashboardEditComponent).find('#editRedditForm').submit(
18730 e.preventDefault();
18731 var thisBasePath = $('#editReddit').val();
18732 if (thisBasePath !== '') {
18733 if (thisBasePath.indexOf(',') !== -1) {
18734 thisBasePath = thisBasePath.replace(/\,/g,'+');
18736 modules['dashboard'].widgetBeingEdited.formerBasePath = modules['dashboard'].widgetBeingEdited.basePath;
18737 modules['dashboard'].widgetBeingEdited.basePath = '/r/'+thisBasePath;
18738 modules['dashboard'].widgetBeingEdited.displayName = $('#editRedditDisplayName').val();
18739 modules['dashboard'].widgetBeingEdited.update();
18740 $('#editReddit').tokenInput('clear');
18741 $('#RESDashboardEditComponent').fadeOut(function() {
18742 $('#editReddit').blur();
18744 modules['dashboard'].widgetBeingEdited.widgetEle.find('.widgetPath').text(modules['dashboard'].widgetBeingEdited.displayName).attr('title','/r/'+thisBasePath);
18745 modules['dashboard'].updateWidget();
18749 $(this.dashboardEditComponent).find('.cancelButton').click(
18751 $('#editReddit').tokenInput('clear');
18752 $('#RESDashboardEditComponent').fadeOut(function() {
18753 $('#editReddit').blur();
18757 $(document.body).append(this.dashboardEditComponent);
18759 showEditForm: function() {
18760 var basePath = modules['dashboard'].widgetBeingEdited.basePath;
18761 var widgetEle = modules['dashboard'].widgetBeingEdited.widgetEle;
18762 $('#editRedditDisplayName').val(modules['dashboard'].widgetBeingEdited.displayName);
18763 var eleTop = $(widgetEle).position().top;
18764 var eleWidth = $(widgetEle).width();
18765 $('#RESDashboardEditComponent').css('top',eleTop+'px').css('left','5px').css('width',(eleWidth+2)+'px').fadeIn('fast');
18766 basePath = basePath.replace(/^\/r\//,'');
18768 var reddits = basePath.split('+');
18769 for (var i=0, len=reddits.length; i<len; i++) {
18775 if (typeof modules['dashboard'].firstEdit === 'undefined') {
18776 $('#editReddit').tokenInput('/api/search_reddit_names.json?app=res', {
18778 queryParam: "query",
18780 allowFreeTagging: true,
18782 onResult: function(response) {
18783 var names = response.names;
18785 for (var i=0, len=names.length; i<len; i++) {
18786 results.push({id: names[i], name: names[i]});
18788 if (names.length === 0) {
18789 var failedQueryValue = $('#token-input-editReddit').val();
18790 results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
18794 onCachedResult: function(response) {
18795 var names = response.names;
18797 for (var i=0, len=names.length; i<len; i++) {
18798 results.push({id: names[i], name: names[i]});
18800 if (names.length === 0) {
18801 var failedQueryValue = $('#token-input-editReddit').val();
18802 results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
18806 prePopulate: prepop,
18807 searchingText: 'Searching for matching reddits - may take a few seconds...',
18808 hintText: 'Type one or more subreddits for which to create a widget.',
18809 resultsFormatter: function(item) {
18810 var thisDesc = item.name;
18811 if (item['failedResult']) thisDesc += ' - [this subreddit may not exist, ensure proper spelling]';
18812 return "<li>" + thisDesc + "</li>"
18815 modules['dashboard'].firstEdit = true;
18817 $('#editReddit').tokenInput('clear');
18818 for (var i=0, len=prepop.length; i<len; i++) {
18819 $('#editReddit').tokenInput('add', prepop[i]);
18823 attachAddComponent: function() {
18824 this.dashboardContents = $('#dashboardContents');
18825 this.dashboardAddComponent = $('<div id="RESDashboardAddComponent" class="RESDashboardComponent" />');
18826 $(this.dashboardAddComponent).html(' \
18827 <div class="addNewWidget">Add a new widget</div> \
18828 <div id="addWidgetButtons"> \
18829 <div class="addButton" id="addMailWidget">+mail widget</div> \
18830 <div class="addButton" id="addUserWidget">+user widget</div> \
18831 <div class="addButton" id="addRedditWidget">+subreddit widget</div> \
18833 <div id="addMailWidgetContainer"> \
18834 <div class="backToWidgetTypes">« back</div> \
18835 <div class="addButton widgetShortcut" widgetPath="/message/inbox/">+inbox</div> \
18836 <div class="addButton widgetShortcut" widgetPath="/message/unread/">+unread</div> \
18837 <div class="addButton widgetShortcut" widgetPath="/message/messages/">+messages</div> \
18838 <div class="addButton widgetShortcut" widgetPath="/message/comments/">+comment replies</div> \
18839 <div class="addButton widgetShortcut" widgetPath="/message/selfreply/">+post replies</div> \
18841 <div id="addUserFormContainer" class="addUserForm"> \
18842 <div class="backToWidgetTypes">« back</div> \
18843 <form id="addUserForm"><input type="text" id="addUser"><input type="submit" class="addButton" value="+add"></form> \
18845 <div id="addRedditFormContainer" class="addRedditForm"> \
18846 <div class="backToWidgetTypes">« back</div> \
18847 <form id="addRedditForm"><input type="text" id="addReddit"><input type="text" id="addRedditDisplayName" placeholder="display name (e.g. stuff)"><input type="submit" class="addButton" value="+add"></form> \
18850 $(this.dashboardAddComponent).find('.backToWidgetTypes').click(function(e) {
18851 $(this).parent().fadeOut(function() {
18852 $('#addWidgetButtons').fadeIn();
18855 $(this.dashboardAddComponent).find('.widgetShortcut').click(function(e) {
18856 var thisBasePath = $(this).attr('widgetPath');
18857 modules['dashboard'].addWidget({
18858 basePath: thisBasePath
18860 $('#addMailWidgetContainer').fadeOut(function() {
18861 $('#addWidgetButtons').fadeIn();
18864 $(this.dashboardAddComponent).find('#addRedditWidget').click(function(e) {
18865 $('#addWidgetButtons').fadeOut(function() {
18866 $('#addRedditFormContainer').fadeIn(function() {
18867 $('#token-input-addReddit').focus();
18871 $(this.dashboardAddComponent).find('#addMailWidget').click(function(e) {
18872 $('#addWidgetButtons').fadeOut(function() {
18873 $('#addMailWidgetContainer').fadeIn();
18876 $(this.dashboardAddComponent).find('#addUserWidget').click(function(e) {
18877 $('#addWidgetButtons').fadeOut(function() {
18878 $('#addUserFormContainer').fadeIn();
18881 var thisEle = $(this.dashboardAddComponent).find('#addReddit');
18882 $(thisEle).tokenInput('/api/search_reddit_names.json?app=res', {
18884 queryParam: "query",
18886 allowFreeTagging: true,
18888 onResult: function(response) {
18889 var names = response.names;
18891 for (var i=0, len=names.length; i<len; i++) {
18892 results.push({id: names[i], name: names[i]});
18894 if (names.length === 0) {
18895 var failedQueryValue = $('#token-input-addReddit').val();
18896 results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
18900 onCachedResult: function(response) {
18901 var names = response.names;
18903 for (var i=0, len=names.length; i<len; i++) {
18904 results.push({id: names[i], name: names[i]});
18906 if (names.length === 0) {
18907 var failedQueryValue = $('#token-input-addReddit').val();
18908 results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
18912 /* prePopulate: prepop, */
18913 searchingText: 'Searching for matching reddits - may take a few seconds...',
18914 hintText: 'Type one or more subreddits for which to create a widget.',
18915 resultsFormatter: function(item) {
18916 var thisDesc = item.name;
18917 if (item['failedResult']) thisDesc += ' - [this subreddit may not exist, ensure proper spelling]';
18918 return "<li>" + thisDesc + "</li>"
18922 $(this.dashboardAddComponent).find('#addRedditForm').submit(
18924 e.preventDefault();
18925 var thisBasePath = $('#addReddit').val();
18926 if (thisBasePath !== '') {
18927 if (thisBasePath.indexOf(',') !== -1) {
18928 thisBasePath = thisBasePath.replace(/\,/g,'+');
18930 var thisDisplayName = ($('#addRedditDisplayName').val()) ? $('#addRedditDisplayName').val() : thisBasePath;
18931 modules['dashboard'].addWidget({
18932 basePath: thisBasePath,
18933 displayName: thisDisplayName
18935 // $('#addReddit').val('').blur();
18936 $('#addReddit').tokenInput('clear');
18937 $('#addRedditFormContainer').fadeOut(function() {
18938 $('#addReddit').blur();
18939 $('#addWidgetButtons').fadeIn();
18944 $(this.dashboardAddComponent).find('#addUserForm').submit(
18946 e.preventDefault();
18947 var thisBasePath = '/user/'+$('#addUser').val();
18948 modules['dashboard'].addWidget({
18949 basePath: thisBasePath
18951 $('#addUser').val('').blur();
18952 $('#addUserFormContainer').fadeOut(function() {
18953 $('#addWidgetButtons').fadeIn();
18958 $(this.dashboardContents).append(this.dashboardAddComponent);
18959 this.dashboardUL = $('<ul id="RESDashboard"></ul>');
18960 $(this.dashboardContents).append(this.dashboardUL);
18962 addWidget: function(optionsObject, isNew) {
18963 if (optionsObject.basePath.slice(0,1) !== '/') optionsObject.basePath = '/r/'+optionsObject.basePath;
18965 for (var i=0, len=this.widgets.length; i<len; i++) {
18966 if (this.widgets[i].basePath == optionsObject.basePath) {
18971 // hide any shortcut button for this widget, since it exists... wait a second, though, or it causes rendering stupidity.
18972 setTimeout(function() {
18973 $('.widgetShortcut[widgetPath="'+optionsObject.basePath+'"]').hide();
18975 if (exists && isNew) {
18976 alert('A widget for '+optionsObject.basePath+' already exists!');
18978 var thisWidget = new this.widgetObject(optionsObject);
18980 modules['dashboard'].saveWidget(thisWidget.optionsObject());
18983 removeWidget: function(optionsObject) {
18984 this.getLatestWidgets();
18985 var exists = false;
18986 for (var i=0, len=modules['dashboard'].widgets.length; i<len; i++) {
18987 if (modules['dashboard'].widgets[i].basePath == optionsObject.basePath) {
18989 $('#'+modules['dashboard'].widgets[i].basePath.replace(/\/|\+/g,'_')).fadeOut('slow', function(ele) {
18992 modules['dashboard'].widgets.splice(i,1);
18993 // show any shortcut button for this widget, since we've now deleted it...
18994 setTimeout(function() {
18995 $('.widgetShortcut[widgetPath="'+optionsObject.basePath+'"]').show();
19001 RESUtils.notification({
19002 moduleID: 'dashboard',
19004 message: 'The widget you just tried to remove does not seem to exist.'
19007 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
19009 saveWidget: function(optionsObject, init) {
19010 this.getLatestWidgets();
19011 var exists = false;
19012 for (var i=0, len=modules['dashboard'].widgets.length; i<len; i++) {
19013 if (modules['dashboard'].widgets[i].basePath == optionsObject.basePath) {
19015 modules['dashboard'].widgets[i] = optionsObject;
19018 if (!exists) modules['dashboard'].widgets.push(optionsObject);
19019 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
19021 updateWidget: function() {
19022 this.getLatestWidgets();
19023 var exists = false;
19024 for (var i=0, len=modules['dashboard'].widgets.length; i<len; i++) {
19025 if (modules['dashboard'].widgets[i].basePath == modules['dashboard'].widgetBeingEdited.formerBasePath) {
19027 delete modules['dashboard'].widgetBeingEdited.formerBasePath;
19028 modules['dashboard'].widgets[i] = modules['dashboard'].widgetBeingEdited.optionsObject();
19031 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
19033 widgetObject: function(widgetOptions) {
19034 var thisWidget = this; // keep a reference because the this keyword can mean different things in different scopes...
19035 thisWidget.basePath = widgetOptions.basePath;
19036 if ((typeof widgetOptions.displayName === 'undefined') || (widgetOptions.displayName === null)) {
19037 widgetOptions.displayName = thisWidget.basePath;
19039 thisWidget.displayName = widgetOptions.displayName;
19040 thisWidget.numPosts = widgetOptions.numPosts || modules['dashboard'].options.defaultPosts.value;
19041 thisWidget.sortBy = widgetOptions.sortBy || modules['dashboard'].options.defaultSort.value;
19042 thisWidget.minimized = widgetOptions.minimized || false;
19043 thisWidget.widgetEle = $('<li class="RESDashboardComponent" id="'+thisWidget.basePath.replace(/\/|\+/g,'_')+'"><div class="RESDashboardComponentScrim"><div class="RESDashboardComponentLoader"><img id="dashboardLoader" src="'+modules['dashboard'].loader+'"><span>querying the server. one moment please.</span></div></div></li>');
19044 var editButtonHTML = (thisWidget.basePath.indexOf('/r/') === -1) ? '' : '<div class="editButton" title="edit"></div>';
19045 thisWidget.header = $('<div class="RESDashboardComponentHeader"><a class="widgetPath" title="'+thisWidget.basePath+'" href="'+thisWidget.basePath+'">'+thisWidget.displayName+'</a></div>');
19046 thisWidget.sortControls = $('<ul class="widgetSortButtons"><li sort="hot">hot</li><li sort="new">new</li><li sort="controversial">controversial</li><li sort="top">top</li></ul>');
19047 // return an optionsObject, which is what we'll store in the modules['dashboard'].widgets array.
19048 thisWidget.optionsObject = function() {
19050 basePath: thisWidget.basePath,
19051 displayName: thisWidget.displayName,
19052 numPosts: thisWidget.numPosts,
19053 sortBy: thisWidget.sortBy,
19054 minimized: thisWidget.minimized
19057 // set the sort by properly...
19058 $(thisWidget.sortControls).find('li[sort='+thisWidget.sortBy+']').addClass('active');
19059 $(thisWidget.sortControls).find('li').click(function(e) {
19060 thisWidget.sortChange($(e.target).attr('sort'));
19062 $(thisWidget.header).append(thisWidget.sortControls);
19063 if ((thisWidget.basePath.indexOf('/r/') !== 0) && (thisWidget.basePath.indexOf('/user/') !== 0)) {
19064 setTimeout(function() {
19065 $(thisWidget.sortControls).hide();
19068 thisWidget.stateControls = $('<ul class="widgetStateButtons"><li class="updateTime"></li><li action="refresh" class="refresh"><div action="refresh"></div></li><li action="refreshAll" class="refreshAll">Refresh All</li><li action="addRow">+row</li><li action="subRow">-row</li><li action="edit" class="editButton"></li><li action="minimize" class="minimize">-</li><li action="delete" class="RESClose">×</li></ul>');
19069 $(thisWidget.stateControls).find('li').click(function (e) {
19070 switch ($(e.target).attr('action')) {
19072 thisWidget.update();
19075 $('li[action="refresh"]').click();
19078 if (thisWidget.numPosts === 10) break;
19079 thisWidget.numPosts++;
19080 if (thisWidget.numPosts === 10) $(thisWidget.stateControls).find('li[action=addRow]').addClass('disabled');
19081 $(thisWidget.stateControls).find('li[action=subRow]').removeClass('disabled');
19082 modules['dashboard'].saveWidget(thisWidget.optionsObject());
19083 thisWidget.update();
19086 if (thisWidget.numPosts === 0) break;
19087 thisWidget.numPosts--;
19088 if (thisWidget.numPosts === 1) $(thisWidget.stateControls).find('li[action=subRow]').addClass('disabled');
19089 $(thisWidget.stateControls).find('li[action=addRow]').removeClass('disabled');
19090 modules['dashboard'].saveWidget(thisWidget.optionsObject());
19091 thisWidget.update();
19094 $(thisWidget.widgetEle).toggleClass('minimized');
19095 if ($(thisWidget.widgetEle).hasClass('minimized')) {
19096 $(e.target).text('+');
19097 thisWidget.minimized = true;
19099 $(e.target).text('-');
19100 thisWidget.minimized = false;
19101 thisWidget.update();
19103 $(thisWidget.contents).parent().slideToggle();
19104 modules['dashboard'].saveWidget(thisWidget.optionsObject());
19107 modules['dashboard'].removeWidget(thisWidget.optionsObject());
19111 $(thisWidget.header).append(thisWidget.stateControls);
19112 thisWidget.sortChange = function(sortBy) {
19113 thisWidget.sortBy = sortBy;
19114 $(thisWidget.header).find('ul.widgetSortButtons li').removeClass('active');
19115 $(thisWidget.header).find('ul.widgetSortButtons li[sort='+sortBy+']').addClass('active');
19116 thisWidget.update();
19117 modules['dashboard'].saveWidget(thisWidget.optionsObject());
19119 thisWidget.edit = function(e) {
19120 modules['dashboard'].widgetBeingEdited = thisWidget;
19121 modules['dashboard'].showEditForm();
19123 $(thisWidget.header).find('.editButton').click(thisWidget.edit);
19124 thisWidget.update = function() {
19125 if (thisWidget.basePath.match(/\/user\//)) {
19126 thisWidget.sortPath = (thisWidget.sortBy === 'hot') ? '/' : '?sort='+thisWidget.sortBy;
19127 } else if (thisWidget.basePath.match(/\/r\//)) {
19128 thisWidget.sortPath = (thisWidget.sortBy === 'hot') ? '/' : '/'+thisWidget.sortBy+'/';
19130 thisWidget.sortPath = '';
19132 thisWidget.url = location.protocol + '//' + location.hostname + '/' + thisWidget.basePath + thisWidget.sortPath;
19133 $(thisWidget.contents).fadeTo('fast',0.25);
19134 $(thisWidget.scrim).fadeIn();
19136 url: thisWidget.url,
19138 limit: thisWidget.numPosts
19140 success: thisWidget.populate,
19141 error: thisWidget.error
19144 thisWidget.container = $('<div class="RESDashboardComponentContainer"><div class="RESDashboardComponentContents"></div></div>');
19145 if (thisWidget.minimized) {
19146 $(thisWidget.container).addClass('minimized');
19147 $(thisWidget.stateControls).find('li.minimize').addClass('minimized').text('+');
19149 thisWidget.scrim = $(thisWidget.widgetEle).find('.RESDashboardComponentScrim');
19150 thisWidget.contents = $(thisWidget.container).find('.RESDashboardComponentContents');
19151 thisWidget.init = function() {
19152 if (RESUtils.currentSubreddit('dashboard')) {
19154 if (!thisWidget.minimized) modules['dashboard'].addToUpdateQueue(thisWidget.update);
19157 thisWidget.draw = function() {
19158 $(thisWidget.widgetEle).append(thisWidget.header);
19159 $(thisWidget.widgetEle).append(thisWidget.container);
19160 if (thisWidget.minimized) $(thisWidget.widgetEle).addClass('minimized');
19161 modules['dashboard'].dashboardUL.prepend(thisWidget.widgetEle);
19162 // $(thisWidget.scrim).fadeIn();
19164 thisWidget.populate = function(response) {
19165 var widgetContent = $(response).find('#siteTable');
19166 $(widgetContent).attr('id','siteTable_'+thisWidget.basePath.replace(/\/|\+/g,'_'));
19167 if (widgetContent.length === 2) widgetContent = widgetContent[1];
19168 $(widgetContent).attr('url',thisWidget.url+'?limit='+thisWidget.numPosts);
19169 if ((widgetContent) && ($(widgetContent).html() !== '')) {
19170 // widgetContent will contain HTML from Reddit's page load. No XSS here or you'd already be hit, can't call escapeHTML on this either and wouldn't help anyhow.
19171 $(thisWidget.contents).html(widgetContent);
19172 $(thisWidget.contents).fadeTo('fast',1);
19173 $(thisWidget.scrim).fadeOut(function(e) {
19174 $(this).hide(); // make sure it is hidden in case the element isn't visible due to being on a different dashboard tab
19176 $(thisWidget.stateControls).find('.updateTime').text('updated: '+RESUtils.niceDateTime());
19178 if (thisWidget.url.indexOf('/message/') !== -1) {
19179 $(thisWidget.contents).html('<div class="widgetNoMail">No messages were found.</div>');
19181 $(thisWidget.contents).html('<div class="error">There were no results returned for this widget. If you made a typo, simply close the widget to delete it. If reddit is just under heavy load, try clicking refresh in a few moments.</div>');
19183 $(thisWidget.contents).fadeTo('fast',1);
19184 $(thisWidget.scrim).fadeOut();
19185 $(thisWidget.stateControls).find('.updateTime').text('updated: '+RESUtils.niceDateTime());
19187 // now run watcher functions from other modules on this content...
19188 RESUtils.watchers.siteTable.forEach(function(callback) {
19189 if (callback) callback(widgetContent[0]);
19193 thisWidget.error = function(xhr, err) {
19194 // alert('There was an error loading data for this widget. Did you type a bad path, perhaps? Removing this widget automatically.');
19195 // modules['dashboard'].removeWidget(thisWidget.optionsObject());
19196 if (xhr.status === 404) {
19197 $(thisWidget.contents).html('<div class="error">This widget received a 404 not found error. You may have made a typo when adding it.</div>');
19199 $(thisWidget.contents).html('<div class="error">There was an error loading data for this widget. Reddit may be under heavy load, or you may have provided an invalid path.</div>');
19201 $(thisWidget.scrim).fadeOut();
19202 $(thisWidget.contents).fadeTo('fast',1);
19205 addDashboardShortcuts: function() {
19206 var subButtons = document.querySelectorAll('.fancy-toggle-button');
19207 for (var h=0, len=subButtons.length; h<len; h++) {
19208 var subButton = subButtons[h];
19209 if ((RESUtils.currentSubreddit().indexOf('+') === -1) && (RESUtils.currentSubreddit() !== 'mod')) {
19210 var thisSubredditFragment = RESUtils.currentSubreddit();
19211 var isMulti = false;
19212 } else if ($(subButton).parent().hasClass('subButtons')) {
19213 var isMulti = true;
19214 var thisSubredditFragment = $(subButton).parent().parent().find('a.title').text();
19216 var isMulti = true;
19217 var thisSubredditFragment = $(subButton).next().text();
19219 if (! ($('#subButtons-'+thisSubredditFragment).length>0)) {
19220 var subButtonsWrapper = $('<div id="subButtons-'+thisSubredditFragment+'" class="subButtons" style="margin: 0 !important;"></div>');
19221 $(subButton).wrap(subButtonsWrapper);
19222 // move this wrapper to the end (after any icons that may exist...)
19224 var theWrap = $(subButton).parent();
19225 $(theWrap).appendTo($(theWrap).parent());
19228 var dashboardToggle = document.createElement('span');
19229 dashboardToggle.setAttribute('class','REStoggle RESDashboardToggle');
19230 dashboardToggle.setAttribute('data-subreddit',thisSubredditFragment);
19232 for (var i=0, sublen=this.widgets.length; i<sublen; i++) {
19233 if ((this.widgets[i]) && (this.widgets[i].basePath.toLowerCase() === '/r/'+thisSubredditFragment.toLowerCase())) {
19239 dashboardToggle.textContent = '-dashboard';
19240 dashboardToggle.setAttribute('title','Remove this subreddit from your dashboard');
19241 dashboardToggle.classList.add('remove');
19243 dashboardToggle.textContent = '+dashboard';
19244 dashboardToggle.setAttribute('title','Add this subreddit to your dashboard');
19246 dashboardToggle.setAttribute('data-subreddit',thisSubredditFragment)
19247 dashboardToggle.addEventListener('click', modules['dashboard'].toggleDashboard, false);
19248 $('#subButtons-'+thisSubredditFragment).append(dashboardToggle);
19249 var next = $('#subButtons-'+thisSubredditFragment).next();
19250 if ($(next).hasClass('title') && (! $('#subButtons-'+thisSubredditFragment).hasClass('swapped'))) {
19251 $('#subButtons-'+thisSubredditFragment).before($(next));
19252 $('#subButtons-'+thisSubredditFragment).addClass('swapped');
19256 toggleDashboard: function(e) {
19257 var thisBasePath = '/r/'+$(e.target).data('subreddit');
19258 if (e.target.classList.contains('remove')) {
19259 modules['dashboard'].removeWidget({
19260 basePath: thisBasePath
19262 e.target.textContent = '+dashboard';
19263 e.target.classList.remove('remove');
19265 modules['dashboard'].addWidget({
19266 basePath: thisBasePath
19268 e.target.textContent = '-dashboard';
19269 RESUtils.notification({
19270 header: 'Dashboard Notification',
19271 moduleID: 'dashboard',
19272 message: 'Dashboard widget added for '+thisBasePath+' <p><a class="RESNotificationButtonBlue" href="/r/Dashboard">view the dashboard</a></p><div class="clear"></div>'
19274 e.target.classList.add('remove');
19277 addTab: function(tabID, tabName) {
19278 $('#siteTable.linklisting').append('<div id="'+tabID+'" class="dashboardPane" />');
19279 $('span.redditname').append('<a id="tab-'+tabID+'" class="dashboardTab" title="'+tabName+'">'+tabName+'</a>');
19280 $('#tab-'+tabID).click(function(e) {
19281 location.hash = tabID;
19282 $('span.redditname a').removeClass('active');
19283 $(this).addClass('active');
19284 $('.dashboardPane').hide();
19285 $('#'+tabID).show();
19290 modules['subredditInfo'] = {
19291 moduleID: 'subredditInfo',
19292 moduleName: 'Subreddit Info',
19298 description: 'Delay, in milliseconds, before hover tooltip loads. Default is 800.'
19303 description: 'Delay, in milliseconds, before hover tooltip fades away. Default is 200.'
19308 description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
19313 description: 'Show date (subreddit created...) in US format (i.e. 08-31-2010)'
19316 description: 'Adds a hover tooltip to subreddits',
19317 isEnabled: function() {
19318 return RESConsole.getModulePrefs(this.moduleID);
19321 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
19323 isMatchURL: function() {
19324 return RESUtils.isMatchURL(this.moduleID);
19326 beforeLoad: function() {
19327 if ((this.isEnabled()) && (this.isMatchURL())) {
19329 css += '.subredditInfoToolTip .subredditLabel { float: left; width: 140px; margin-bottom: 12px; }';
19330 css += '.subredditInfoToolTip .subredditDetail { float: left; width: 240px; margin-bottom: 12px; }';
19331 css += '.subredditInfoToolTip .blueButton { float: right; margin-left: 8px; }';
19332 css += '.subredditInfoToolTip .redButton { float: right; margin-left: 8px; }';
19333 RESUtils.addCSS(css);
19337 if ((this.isEnabled()) && (this.isMatchURL())) {
19338 // create a cache for subreddit data so we only load it once even if the hover is triggered many times
19339 this.subredditInfoCache = [];
19340 this.srRe = /\/r\/(\w+)(?:\/(new|rising|controversial|top))?\/?$/i;
19342 // get subreddit links and add event listeners...
19343 this.addListeners();
19344 RESUtils.watchForElement('siteTable', modules['subredditInfo'].addListeners);
19347 addListeners: function(ele) {
19348 var ele = ele || document.body;
19349 var subredditLinks = document.body.querySelectorAll('.listing-page a.subreddit, .comment .md a[href^="/r/"]');
19350 if (subredditLinks) {
19351 var len=subredditLinks.length;
19352 for (var i=0; i<len; i++) {
19353 var thisSRLink = subredditLinks[i];
19354 if (this.srRe.test(thisSRLink.href)) {
19355 thisSRLink.addEventListener('mouseover', function(e) {
19356 RESUtils.hover.begin(e.target, {
19358 openDelay: modules['subredditInfo'].options.hoverDelay.value,
19359 fadeDelay: modules['subredditInfo'].options.fadeDelay.value,
19360 fadeSpeed: modules['subredditInfo'].options.fadeSpeed.value
19361 }, modules['subredditInfo'].showSubredditInfo, {});
19367 showSubredditInfo: function(def, obj, context) {
19368 var mod = modules['subredditInfo'];
19369 var thisSubreddit = obj.textContent.replace("/r/","");
19370 var header = document.createDocumentFragment();
19371 var link = $('<a href="/r/'+escapeHTML(thisSubreddit)+'">/r/' + escapeHTML(thisSubreddit) + '</a>');
19372 header.appendChild(link[0]);
19373 if (RESUtils.loggedInUser()) {
19374 var subscribeToggle = $('<span />');
19376 .attr('id', 'RESHoverInfoSubscriptionButton')
19377 .addClass('RESFilterToggle')
19378 .css('margin-left', '12px')
19380 .on('click', modules['subredditInfo'].toggleSubscription);
19381 modules['subredditInfo'].updateToggleButton(subscribeToggle, false);
19383 header.appendChild(subscribeToggle[0]);
19386 <div class="subredditInfoToolTip">\
19387 <a class="hoverSubreddit" href="/user/'+escapeHTML(thisSubreddit)+'">'+escapeHTML(thisSubreddit)+'</a>:<br>\
19388 <img src="'+RESConsole.loader+'"> loading...\
19390 def.notify(header, null);
19391 if (typeof mod.subredditInfoCache[thisSubreddit] !== 'undefined') {
19392 mod.writeSubredditInfo(mod.subredditInfoCache[thisSubreddit], def);
19394 GM_xmlhttpRequest({
19396 url: location.protocol + "//"+location.hostname+"/r/" + thisSubreddit + "/about.json?app=res",
19397 onload: function(response) {
19398 var thisResponse = safeJSON.parse(response.responseText, null, true);
19399 if (thisResponse) {
19400 mod.updateCache(thisSubreddit, thisResponse);
19401 mod.writeSubredditInfo(thisResponse, def);
19403 mod.writeSubredditInfo({}, def);
19409 updateCache: function(subreddit, data) {
19410 subreddit = subreddit.toLowerCase();
19412 data = { data : data };
19414 this.subredditInfoCache = this.subredditInfoCache || [];
19415 this.subredditInfoCache[subreddit] = $.extend(true, {}, this.subredditInfoCache[subreddit], data);
19417 writeSubredditInfo: function(jsonData, deferred) {
19418 if (!jsonData.data) {
19419 var srHTML = '<div class="subredditInfoToolTip">Subreddit not found</div>';
19420 var newBody = $(srHTML);
19421 deferred.resolve(null, newBody)
19424 var utctime = jsonData.data.created_utc;
19425 var d = new Date(utctime * 1000);
19427 jsonData.data.over18 === true ? isOver18 = 'Yes' : isOver18 = 'No';
19428 var srHTML = '<div class="subredditInfoToolTip">';
19429 srHTML += '<div class="subredditLabel">Subreddit created:</div> <div class="subredditDetail">' + RESUtils.niceDate(d, this.options.USDateFormat.value) + ' (' + RESUtils.niceDateDiff(d) + ')</div>';
19430 srHTML += '<div class="subredditLabel">Subscribers:</div> <div class="subredditDetail">' + RESUtils.addCommas(jsonData.data.subscribers) + '</div>';
19431 srHTML += '<div class="subredditLabel">Title:</div> <div class="subredditDetail">' + escapeHTML(jsonData.data.title) + '</div>';
19432 srHTML += '<div class="subredditLabel">Over 18:</div> <div class="subredditDetail">' + escapeHTML(isOver18) + '</div>';
19433 // srHTML += '<div class="subredditLabel">Description:</div> <div class="subredditDetail">' + jsonData.data.description + '</div>';
19434 srHTML += '<div class="clear"></div><div id="subTooltipButtons" class="bottomButtons">';
19435 srHTML += '<div class="clear"></div></div>'; // closes bottomButtons div
19436 srHTML += '</div>';
19438 var newBody = $(srHTML);
19439 // bottom buttons will include: +filter +shortcut +dashboard (maybe sub/unsub too?)
19440 if (modules['subredditManager'].isEnabled()) {
19441 var theSC = document.createElement('span');
19442 theSC.setAttribute('style','display: inline-block !important;');
19443 theSC.setAttribute('class','REStoggle RESshortcut RESshortcutside');
19444 theSC.setAttribute('data-subreddit',jsonData.data.display_name.toLowerCase());
19446 for (var i=0, len=modules['subredditManager'].mySubredditShortcuts.length; i<len; i++) {
19447 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() == jsonData.data.display_name.toLowerCase()) {
19453 theSC.textContent = '-shortcut';
19454 theSC.setAttribute('title','Remove this subreddit from your shortcut bar');
19455 theSC.classList.add('remove');
19457 theSC.textContent = '+shortcut';
19458 theSC.setAttribute('title','Add this subreddit to your shortcut bar');
19460 theSC.addEventListener('click', modules['subredditManager'].toggleSubredditShortcut, false);
19462 newBody.find('#subTooltipButtons').append(theSC);
19464 if (modules['dashboard'].isEnabled()) {
19465 var dashboardToggle = document.createElement('span');
19466 dashboardToggle.setAttribute('class','RESDashboardToggle');
19467 dashboardToggle.setAttribute('data-subreddit',jsonData.data.display_name.toLowerCase());
19469 for (var i=0, len=modules['dashboard'].widgets.length; i<len; i++) {
19470 if ((modules['dashboard'].widgets[i]) && (modules['dashboard'].widgets[i].basePath.toLowerCase() === '/r/'+jsonData.data.display_name.toLowerCase())) {
19476 dashboardToggle.textContent = '-dashboard';
19477 dashboardToggle.setAttribute('title','Remove this subreddit from your dashboard');
19478 dashboardToggle.classList.add('remove');
19480 dashboardToggle.textContent = '+dashboard';
19481 dashboardToggle.setAttribute('title','Add this subreddit to your dashboard');
19483 dashboardToggle.addEventListener('click', modules['dashboard'].toggleDashboard, false);
19484 newBody.find('#subTooltipButtons').append(dashboardToggle);
19486 if (modules['filteReddit'].isEnabled()) {
19487 var filterToggle = document.createElement('span');
19488 filterToggle.setAttribute('class','RESFilterToggle');
19489 filterToggle.setAttribute('data-subreddit',jsonData.data.display_name.toLowerCase());
19491 var filteredReddits = modules['filteReddit'].options.subreddits.value;
19492 for (var i=0, len=filteredReddits.length; i<len; i++) {
19493 if ((filteredReddits[i]) && (filteredReddits[i][0].toLowerCase() == jsonData.data.display_name.toLowerCase())) {
19499 filterToggle.textContent = '-filter';
19500 filterToggle.setAttribute('title','Stop filtering from /r/all and /domain/*');
19501 filterToggle.classList.add('remove');
19503 filterToggle.textContent = '+filter';
19504 filterToggle.setAttribute('title','Filter this subreddit from /r/all and /domain/*');
19506 filterToggle.addEventListener('click', modules['filteReddit'].toggleFilter, false);
19507 newBody.find('#subTooltipButtons').append(filterToggle);
19510 if (RESUtils.loggedInUser()) {
19511 var subscribed = !!jsonData.data.user_is_subscriber;
19513 var subscribeToggle = $('#RESHoverInfoSubscriptionButton');
19514 subscribeToggle.attr('data-subreddit',jsonData.data.display_name.toLowerCase());
19515 modules['subredditInfo'].updateToggleButton(subscribeToggle, subscribed);
19516 subscribeToggle.fadeIn('fast');
19519 deferred.resolve(null, newBody)
19521 updateToggleButton: function(toggleButton, subscribed) {
19522 if (toggleButton instanceof jQuery) toggleButton = toggleButton[0];
19523 var toggleOn = '+subscribe';
19524 var toggleOff = '-unsubscribe';
19526 toggleButton.textContent = toggleOff;
19527 toggleButton.classList.add('remove');
19529 toggleButton.textContent = toggleOn;
19530 toggleButton.classList.remove('remove');
19533 toggleSubscription: function(e) {
19535 var subscribeToggle = e.target;
19536 var subreddit = subscribeToggle.getAttribute('data-subreddit').toLowerCase();
19537 var subredditData = modules['subredditInfo'].subredditInfoCache[subreddit].data;
19538 var subscribing = !subredditData.user_is_subscriber;
19540 modules['subredditInfo'].updateToggleButton(subscribeToggle, subscribing);
19542 modules['subredditManager'].subscribeToSubreddit(subredditData.name, subscribing);
19543 modules['subredditInfo'].updateCache(subreddit, { 'user_is_subscriber': subscribing });
19545 }; // note: you NEED this semicolon at the end!
19550 * CommentHidePersistor - stores hidden comments in localStorage and re-hides
19551 * them on reload of the page.
19553 m_chp = modules['commentHidePersistor'] = {
19554 moduleID: 'commentHidePersistor',
19555 moduleName: 'Comment Hide Persistor',
19556 category: 'Comments',
19557 description: 'Saves the state of hidden comments across page views.',
19558 allHiddenThings: {},
19561 hiddenThingsKey: window.location.href,
19566 isEnabled: function () {
19567 return RESConsole.getModulePrefs(this.moduleID);
19570 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
19571 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
19573 isMatchURL: function () {
19574 return RESUtils.isMatchURL(this.moduleID);
19577 if ((this.isEnabled()) && (this.isMatchURL())) {
19578 m_chp.bindToHideLinks();
19579 m_chp.hideHiddenThings();
19582 bindToHideLinks: function () {
19584 * For every expand/collapse link, add a click listener that will
19585 * store or remove the comment ID from our list of hidden comments.
19587 $('body').on('click', 'a.expand', function () {
19588 var thing = $(this).parents('.thing'),
19589 thingId = thing.data('fullname'),
19590 collapsing = !$(this).parent().is('.collapsed');
19592 /* Add our key to pages interacted with, for potential pruning
19594 if (m_chp.hiddenKeys.indexOf(m_chp.hiddenThingsKey) === -1) {
19595 m_chp.hiddenKeys.push(m_chp.hiddenThingsKey);
19599 m_chp.addHiddenThing(thingId);
19601 m_chp.removeHiddenThing(thingId);
19605 loadHiddenThings: function () {
19606 var hidePersistorJson = RESStorage.getItem('RESmodules.commentHidePersistor.hidePersistor')
19608 if (hidePersistorJson) {
19610 m_chp.hidePersistorData = safeJSON.parse(hidePersistorJson)
19611 m_chp.allHiddenThings = m_chp.hidePersistorData['hiddenThings']
19612 m_chp.hiddenKeys = m_chp.hidePersistorData['hiddenKeys']
19615 * Prune allHiddenThings of old content so it doesn't get
19618 if (m_chp.hiddenKeys.length > m_chp.maxKeys) {
19619 var pruneStart = m_chp.maxKeys - m_chp.pruneKeysTo,
19620 newHiddenThings = {},
19621 newHiddenKeys = [];
19623 /* Recreate our object as a subset of the original */
19624 for (var i=pruneStart; i < m_chp.hiddenKeys.length; i++) {
19625 var hiddenKey = m_chp.hiddenKeys[i];
19626 newHiddenKeys.push(hiddenKey);
19627 newHiddenThings[hiddenKey] = m_chp.allHiddenThings[hiddenKey];
19629 m_chp.allHiddenThings = newHiddenThings;
19630 m_chp.hiddenKeys = newHiddenKeys;
19631 m_chp.syncHiddenThings();
19634 if (typeof m_chp.allHiddenThings[m_chp.hiddenThingsKey] !== 'undefined') {
19635 m_chp.hiddenThings = m_chp.allHiddenThings[m_chp.hiddenThingsKey];
19641 addHiddenThing: function (thingId) {
19642 var i = m_chp.hiddenThings.indexOf(thingId);
19644 m_chp.hiddenThings.push(thingId);
19646 m_chp.syncHiddenThings();
19648 removeHiddenThing: function (thingId) {
19649 var i = m_chp.hiddenThings.indexOf(thingId);
19651 m_chp.hiddenThings.splice(i, 1);
19653 m_chp.syncHiddenThings();
19655 syncHiddenThings: function () {
19656 var hidePersistorData;
19657 m_chp.allHiddenThings[m_chp.hiddenThingsKey] = m_chp.hiddenThings;
19658 hidePersistorData = {
19659 'hiddenThings': m_chp.allHiddenThings,
19660 'hiddenKeys': m_chp.hiddenKeys
19662 RESStorage.setItem('RESmodules.commentHidePersistor.hidePersistor', JSON.stringify(hidePersistorData));
19664 hideHiddenThings: function () {
19665 m_chp.loadHiddenThings();
19667 for(var i=0, il=m_chp.hiddenThings.length; i < il; i++) {
19668 var thingId = m_chp.hiddenThings[i],
19669 // $hideLink = $('div.id-' + thingId + ':first > div.entry div.noncollapsed a.expand');
19670 // changed how this is grabbed and clicked due to firefox not working properly with it.
19671 $hideLink = document.querySelector('div.id-' + thingId + ' > div.entry div.noncollapsed a.expand');
19674 * Zero-length timeout to defer this action until after the
19675 * other modules have finished. For some reason without
19676 * deferring the hide was conflicting with the
19677 * commentNavToggle width.
19679 (function ($hideLink) {
19680 window.setTimeout(function () {
19681 // $hideLink.click();
19682 RESUtils.click($hideLink);
19693 modules['bitcointip'] = {
19694 moduleID: 'bitcointip',
19695 moduleName: 'bitcointip',
19697 disabledByDefault: true,
19698 description: 'Send <a href="http://bitcoin.org/" target="_blank">' +
19699 'bitcoin</a> to other redditors via <a href="/r/bitcointip" ' +
19700 'target="_blank">bitcointip</a>. <br><br>' +
19701 'For more information, visit <a href="/r/bitcointip" ' +
19702 'target="_blank">/r/bitcointip</a> or <a href="/13iykn" ' +
19703 'target="_blank">read the documentation</a>.',
19706 name: 'Default Tip',
19709 description: 'Default tip amount in the form of ' +
19710 '"[value] [units]", e.g. "0.01 BTC"'
19713 name: 'Add "tip bitcoins" Button',
19716 description: 'Attach "tip bitcoins" button to comments'
19719 name: 'Hide Bot Verifications',
19722 description: 'Hide bot verifications'
19725 name: 'Tip Status Format',
19728 { name: 'detailed', value: 'detailed' },
19729 { name: 'basic', value: 'basic' },
19730 { name: 'none', value: 'none' }
19733 description: 'Tip status - level of detail'
19736 name: 'Preferred Currency',
19739 { name: 'BTC', value: 'BTC' },
19740 { name: 'USD', value: 'USD' },
19741 { name: 'JPY', value: 'JPY' },
19742 { name: 'GBP', value: 'GBP' },
19743 { name: 'EUR', value: 'EUR' }
19746 description: 'Preferred currency units'
19749 name: 'Display Balance',
19752 description: 'Display balance'
19755 name: 'Display Enabled Subreddits',
19758 description: 'Display enabled subreddits'
19761 name: 'Known User Addresses',
19763 addRowText: '+add address',
19765 {name: 'user', type: 'text'},
19766 {name: 'address', type: 'text'}
19769 /* ['skeeto', '1...'] */
19771 description: 'Mapping of usernames to bitcoin addresses'
19773 fetchWalletAddress: {
19774 text: 'Search private messages',
19775 description: "Search private messages for bitcoin wallet associated with the current username." +
19776 "<p>You must be logged in to search.</p>" +
19777 "<p>After clicking the button, you must reload the page to see newly-found addresses.</p>",
19779 callback: null // populated when module loads
19782 isEnabled: function() {
19783 return RESConsole.getModulePrefs(this.moduleID);
19786 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
19789 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*\/user\/bitcointip\/?/i
19791 isMatchURL: function() {
19792 return RESUtils.isMatchURL(this.moduleID);
19794 beforeLoad: function() {
19795 this.options.fetchWalletAddress.callback = this.fetchAddressForCurrentUser.bind(this);
19796 RESUtils.addCSS('.tip-bitcoins { cursor: pointer; }');
19797 RESUtils.addCSS('.tips-enabled-icon { cursor: help; }');
19798 RESUtils.addCSS('#tip-menu { display: none; position: absolute; top: 0; left: 0; }');
19799 // fix weird z-indexing issue caused by reddit's default .dropdown class
19800 RESUtils.addCSS('.tip-wrapper .dropdown { position: static; }');
19804 if (!this.isEnabled() || !this.isMatchURL()) {
19808 if (this.options.status.value === 'basic') {
19809 this.icons.pending = this.icons.completed;
19810 this.icons.reversed = this.icons.completed;
19813 if (this.options.subreddit.value) {
19814 this.attachSubredditIndicator();
19817 if (this.options.balance.value) {
19818 this.attachBalance();
19821 if (RESUtils.currentSubreddit() === 'bitcointip') {
19822 this.injectBotStatus();
19825 if (RESUtils.pageType() === 'comments') {
19826 if (this.options.attachButtons.value) {
19827 this.attachTipButtons();
19828 RESUtils.watchForElement('newComments', modules['bitcointip'].attachTipButtons.bind(this));
19829 this.attachTipMenu();
19832 if (this.options.hide.value) {
19833 this.hideVerifications();
19834 RESUtils.watchForElement('newComments', modules['bitcointip'].hideVerifications.bind(this));
19838 if (this.options.status.value !== 'none') {
19839 this.scanForTips();
19840 RESUtils.watchForElement('newComments', this.scanForTips.bind(this));
19845 save: function save() {
19846 var json = JSON.stringify(this.options);
19847 RESStorage.setItem('RESoptions.bitcoinTip', json);
19850 load: function load() {
19851 var json = RESStorage.getItem('RESoptions.bitcoinTip');
19853 this.options = JSON.parse(json);
19858 /** Specifies how to find tips. */
19859 tipregex: /\+((\/u\/)?bitcointip|bitcoin|tip|btctip|bittip|btc)/i,
19860 tipregexFun: /(\+((?!0)(\d{1,4})) (point|internet|upcoin))/i,
19862 /** How many milliseconds until the bot is considered down. */
19863 botDownThreshold: 15 * 60 * 1000,
19865 /** Bitcointip API endpoints. */
19867 gettips: '//bitcointip.net/api/gettips.php',
19868 gettipped: '//bitcointip.net/api/gettipped.php',
19869 subreddits: '//bitcointip.net/api/subreddits.php',
19870 balance: '//bitcointip.net/api/balance.php'
19873 /** Encoded tipping icons. */
19875 completed: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAMAAABFNRROAAAAt1BMVEX///8AAAAAyAAAuwAAwQcAvAcAvwAAwQYAyAUAxAUAxwQAwgQAvAMAxQYAvwYAxQYAxwU5yT060j460j871T89wUE9wkFGokdGu0hIzExJl09JmE9JxExJxE1K1U9K1k5Ll09LmVNMmVNM2FBNmlRRx1NSzlRTqlVUslZU1ldVq1hVrFdV2FhWrFhX21pZqlphrWJh3WRotGtrqm1stW91sXd2t3h5t3urz6zA2sHA28HG3sf4+PhvgZhQAAAAEXRSTlMAARweJSYoLTM0O0dMU1dYbkVIv+oAAACKSURBVHjaVc7XEoIwEIXhFRED1tBUxBaPFSyxK3n/5zIBb/yv9pudnVky2Ywxm345MHkVXByllPm4W24qrLbzdo1sLPPRepc+XlnSIAuz9DQYPtXnkLhUF/ysrndV3CYLRpbg2VtpxFMwfRfEl8IghEPUhB9t9lEQoke6FnzONfpU5kEIoKOn/z+/pREPWTic38sAAAAASUVORK5CYII=",
19876 cancelled: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAMAAABFNRROAAAAQlBMVEX///+qAAAAAAC/AADIABSaTU3YMDDcPj7cSEjeUFDiZGTld3fmfHzoiorqkJDqlpbupKTuqqr99fX99/f+/Pz////kWqLlAAAABXRSTlMAAwcIM6KYVMQAAABfSURBVHjaXc7JDsAgCEVRsYpIBzro//9qHyHpond3AgkklIuXPKJcqleIIEB6FwEhQEW0t4rlUtt+22ZTMQ09NqZyiK8BtBCvc9iDWegY526hBVRmdcQ9RgD9f/G+P1+JEwRF2vKhRgAAAABJRU5ErkJggg==",
19877 tipped: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAwAAAANCAMAAACq939wAAAA/FBMVEWqVQCqcQCZZgCLXQCdYgDMjACOXACOXwCacQCfcQCqegCwggChggCnfwCwggCthACtgQC9iACleQC+iwDMlgDJlACmfgDDiwDBjADHlQDJnQDFlQDKmAChfRGjgBOmfhKmghKnghOqhBKthxOviROvjB+vjCGvjCOwiRSwihixixWxjSGziBOzkSmzky+0kSa0kiy3jxW8kxS8mCi8n0i8oEq9lBe9mSvOoBbUrTTUrTjbukXcsTDctDrdtj/exHbexXDfwWXfwmvjrBnksRjksx7lrhnqx17qyWTqymrq377rz3nr2qftuiHtvSv67cD67sj+997/+OX///8rcy1sAAAAHXRSTlMDCQoLDRQkKystMDc5Oj0+QUlKS0tMTVFSV15/i6wTI/gAAACWSURBVHjaHcrnAoFQGADQryI7sjNKN0RKZJVNQ8io938YN+f3ASDp1B9NAhD15UzXNH26KhNQXZyDBxZcNlloKadvFIbR56owUOFV9425ai8PrGwZaITQeisXoKHs7k/MP44ZaHYl54U5El8EdmjNEWbEjesf/Ljd9v0S1ETBtD3PNgUxB8nOQIybOGknAKgMy2FsmoIflIEZdK7PshkAAAAASUVORK5CYII=",
19878 pending: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAABWUlEQVR4nI2Sy0sCURTGD6S2jjYFrdy0DNpEhokb8zFm5YyaO6NFYNGqF/0hPZYtR79FUbgw0BFtDKIgUCSpv8Od3XtGJzWDBj64h/l+954XdbtdGhQZkzNUd7ptifiXZygo0Wz0WsWoyHTMj4Wo6nRLQ7KdRuZz15bWSiF0GQOVXJ4hqP/COGDTjEO9SyByIcDHiUXiT+QsAaW1wabgi4KtVxVqM4lQVcFx4RS5tzy0vIgZFDVTnaYkFG6us2lbTyNws4ZAMYizwjk6nQ7KbQOJfMqCRBlERZpWruJYfvYigx02ZfUDHN2e8Pnpy8T+w6G4MIqI8HFH5Ut9SKZQ/jDYPAh4K36EGzGrkwz1avK8+/jn3n2WzaPASsNnQaJpvYG65ixwFV7Dj7iuQcul+Cwvs4Ga1fafOVUcC31Qpio1BJjO0PiNEJPn9osapeyNqLmW/lyj/+7eN1qRZT0kKLSqAAAAAElFTkSuQmCC",
19879 reversed: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA0AAAANCAYAAABy6+R8AAABOklEQVR42p2SvU4CQRSFj+9h7DQ2VmsDCy0/Cw2wsNtuRwixIiQ8CZaWGDBaaAiJyVAg2xi1AaTQ19jyOHeSIbLBxuIkM7Pn23PvnQHJPSngNAYcK9mnPWng/LpaZVoadg9CC+BSDNsg4FcU7bRpNjkslaiAYAfZhL+AuFbjQ6PBYaXCZ6AK4Mj0IMBGH4rptVjkW73Ote9zUS5z2u/zfTzmRBL1XoNniIFjgdaeZ0yjMORNocCZ1nQwYJIk3CrFSatlIGkDM+BEouNMhndRZEyjTof3vZ5Zfy+XfOp2zQ+ND3BMkoWkhE+lxLwHzPN5rjzPTtLZ9fSRzZqPj+22MaeBlesaSIZmp3dhQZXLcaQHcev7sjZnFngBwr17mgNFC+pSRRawZV0dfBFy89ogDYvEbBMav33/ens/XHaDp7U/bFsAAAAASUVORK5CYII="
19882 /** Specifies how to display different currencies. */
19884 USD: {unit: 'US$', precision: 2},
19885 BTC: {unit: '฿'},
19887 GBP: {unit: '£', precision: 2},
19888 EUR: {unit: '€', precision: 2},
19889 AUD: {unit: 'A$', precision: 2},
19890 CAD: {unit: 'C$', precision: 2}
19893 /** Return a DOM element to separate items in the user bar. */
19894 separator: function() {
19895 return $('<span>|</span>').addClass('separator');
19898 /** Convert a quantity into a string. */
19899 quantityString: function quantityString(object) {
19900 var pref = this.options.currency.value.toUpperCase();
19901 var unit = this.currencies[pref];
19902 var amount = object['amount' + pref] || object['balance' + pref];
19903 if (amount == null) {
19904 amount = object['amountBTC'] || object['balanceBTC'];
19905 unit = this.currencies['BTC'];
19907 if (unit.precision) {
19908 amount = parseFloat(amount).toFixed(unit.precision);
19910 return unit.unit + amount;
19913 tipPublicly: function tipPublicly($target) {
19915 if ($target.closest('.link').length > 0) { /* Post */
19916 form = $('.commentarea .usertext:first');
19917 } else { /* Comment */
19918 var replyButton = $target.closest('ul').find('a[onclick*="reply"]');
19919 RESUtils.click(replyButton[0]);
19920 form = $target.closest('.thing').find('FORM.usertext.cloneable:first');
19922 var textarea = form.find('textarea');
19923 if (!textarea.val().match(this.tipregex)) {
19924 textarea.val(textarea.val() + '\n\n+/u/bitcointip ' + this.options.baseTip.value);
19925 RESUtils.setCursorPosition(textarea, 0);
19929 tipPrivately: function tipPrivately($target) {
19931 if ($target.closest('.link').length > 0) { /* Post */
19932 form = $('.commentarea .usertext:first');
19934 form = $target.closest('.thing').find(".child .usertext:first");
19936 if (form.length > 0 && form.find('textarea').val()) {
19937 /* Confirm if a comment has been entered. */
19938 if (!confirm('Really leave this page to tip privately?')) {
19942 var user = $target.closest('.thing').find('.author:first').text();
19943 var msg = encodeURIComponent('+/u/bitcointip @' + user + ' ' + this.options.baseTip.value);
19944 var url = '/message/compose?to=bitcointip&subject=Tip&message=' + msg;
19945 window.location = url;
19948 attachTipButtons: function attachTipButtons(ele) {
19949 ele = ele || document.body;
19951 if (!module.tipButton) {
19952 module.tipButton = $(
19953 '<span class="tip-wrapper">' +
19954 '<div class="dropdown">' +
19955 '<a class="tip-bitcoins login-required" title="Click to give a bitcoin tip">bitcointip</a>' +
19958 module.tipButton.bind('click', function(e) {
19959 modules['bitcointip'].toggleTipMenu(e.target);
19964 /* Add the "tip bitcoins" button after "give gold". */
19965 var allGiveGoldLinks = ele.querySelectorAll('a.give-gold');
19966 RESUtils.forEachChunked(allGiveGoldLinks, 15, 1000, function(giveGold, i, array) {
19967 $(giveGold).parent().after($('<li/>')
19968 .append(modules['bitcointip'].tipButton.clone(true)));
19971 if (!module.attachedPostTipButton) {
19972 module.attachedPostTipButton = true; // signifies either "attached button" or "decided not to attach button"
19974 if (!RESUtils.isCommentPermalinkPage() && $('.link').length === 1) {
19975 // Viewing full comments on a submission, so user can comment on post
19976 $('.link ul.buttons .share').after($('<li/>')
19977 .append(modules['bitcointip'].tipButton.clone(true)));
19983 attachTipMenu: function() {
19985 $('<div id="tip-menu" class="drop-choices">' +
19986 '<a class="choice tip-publicly" href="javascript:void(0);">tip publicly</a>' +
19987 '<a class="choice tip-privately" href="javascript:void(0);">tip privately</a>' +
19990 if (modules['settingsNavigation']) { // affordance for userscript mode
19991 this.tipMenu.append(
19992 modules['settingsNavigation'].makeUrlHashLink(this.moduleID, null,
19993 '<img src="' + this.icons.tipped + '"> bitcointip', 'choice')
19996 $(document.body).append(this.tipMenu);
19998 this.tipMenu.find('a').click(function(event) {
19999 modules['bitcointip'].toggleTipMenu();
20002 this.tipMenu.find('.tip-publicly').click(function(event) {
20003 event.preventDefault();
20004 modules['bitcointip'].tipPublicly($(modules['bitcointip'].lastToggle));
20007 this.tipMenu.find('.tip-privately').click(function(event) {
20008 event.preventDefault();
20009 modules['bitcointip'].tipPrivately($(modules['bitcointip'].lastToggle));
20014 toggleTipMenu: function(ele) {
20015 var tipMenu = modules['bitcointip'].tipMenu;
20017 if (!ele || ele.length === 0) {
20022 var thisXY = $(ele).offset();
20023 var thisHeight = $(ele).height();
20024 // if already visible and we've clicked a different trigger, hide first, then show after the move.
20025 if ((tipMenu.is(':visible')) && (modules['bitcointip'].lastToggle !== ele)) {
20029 top: (thisXY.top+thisHeight)+'px',
20030 left: thisXY.left+'px'
20033 modules['bitcointip'].lastToggle = ele;
20036 attachSubredditIndicator: function() {
20037 var subreddit = RESUtils.currentSubreddit();
20038 if (subreddit && this.getAddress(RESUtils.loggedInUser())) {
20039 $.getJSON(this.api.subreddits, function(data) {
20040 if (data.subreddits.indexOf(subreddit.toLowerCase()) !== -1) {
20041 $('#header-bottom-right form.logout')
20042 .before(this.separator()).prev()
20043 .before($('<img/>').attr({
20044 'src': this.icons.tipped,
20045 'class': 'tips-enabled-icon',
20046 'style': 'vertical-align: text-bottom;',
20047 'title': 'Tips enabled in this subreddit.'
20054 hideVerifications: function hideVerifications(ele) {
20055 ele = ele || document.body;
20057 /* t2_7vw3n is u/bitcointip. */
20059 var botComments = $(ele).find('a.id-t2_7vw3n').closest('.comment');
20060 RESUtils.forEachChunked(botComments, 15, 1000, function(botComment, i, array) {
20061 var $this = $(botComment);
20062 var isTarget = $this.find('form:first').hasClass('border');
20063 if (isTarget) return;
20065 var hasReplies = $this.find('.comment').length > 0;
20066 if (hasReplies) return;
20068 $this.find('.expand').eq(2).click();
20072 toggleCurrency: function() {
20073 var units = Object.keys(this.currencies);
20074 var i = (units.indexOf(this.options.currency.value) + 1) % units.length;
20075 this.options.currency.value = units[i];
20079 getAddress: function getAddress(user) {
20080 user = user || RESUtils.loggedInUser();
20081 var address = null;
20082 this.options.address.value.forEach(function(row) {
20083 if (row[0] === user) address = row[1];
20088 setAddress: function setAddress(user, address) {
20089 user = user || RESUtils.loggedInUser();
20091 this.options.address.value.forEach(function(row) {
20092 if (row[0] === user) {
20097 if (user && !set) {
20098 this.options.address.value.push([user, address]);
20104 attachBalance: function attachBalance() {
20105 var user = RESUtils.loggedInUser();
20106 var address = this.getAddress(user);
20107 if (!address) return;
20108 var bitcointip = this;
20110 $.getJSON(this.api.balance, {
20113 }, function (balance) {
20114 if (!('balanceBTC' in balance)) {
20115 return; /* Probably have the address wrong! */
20117 $('#header-bottom-right form.logout')
20118 .before(bitcointip.separator()).prev()
20119 .before($('<a/>').attr({
20122 }).click(function() {
20123 bitcointip.toggleCurrency();
20124 $(this).text(bitcointip.quantityString(balance));
20125 }).text(bitcointip.quantityString(balance)));
20129 fetchAddressForCurrentUser: function () {
20130 var user = RESUtils.loggedInUser();
20132 RESUtils.notification({
20133 moduleID: 'bitcointip',
20134 optionKey: 'fetchWalletAddress',
20136 message: 'Log in, then try again.'
20140 this.fetchAddress(user, function(address) {
20142 modules['bitcointip'].setAddress(user, address);
20143 RESUtils.notification({
20144 moduleID: 'bitcointip',
20145 optionKey: 'address',
20146 message: 'Found address ' + address + ' for user ' + user +
20147 '<br><br>Your adress will appear in RES settings after you refresh the page.'
20150 RESUtils.notification({
20151 moduleID: 'bitcointip',
20153 message: 'Could not find address for user ' + user
20158 RESUtils.notification({
20159 moduleID: 'bitcointip',
20160 optionKey: 'fetchWalletAddress',
20161 message: 'Searching your private messages for a bitcoin wallet address. ' +
20162 '<br><br>Reload the page to see if a wallet was found.'
20166 fetchAddress: function fetchAddress(user, callback) {
20167 user = user || RESUtils.loggedInUser();
20168 callback = callback || function nop() {};
20170 $.getJSON('/message/messages.json', function(messages) {
20171 /* Search messages for a bitcointip response. */
20172 var address = messages.data.children.filter(function (message) {
20173 return message.data.author === 'bitcointip';
20174 }).map(function (message) {
20175 var pattern = /Deposit Address: \| \[\*\*([a-zA-Z0-9]+)\*\*\]/;
20176 var address = message.data.body.match(pattern);
20182 }).filter(function(x) { return x; })[0]; // Use the most recent
20184 this.setAddress(user, address);
20192 scanForTips: function(ele) {
20193 ele = ele || document.body;
20194 var tips = this.getTips(this.tipregex, ele);
20195 var fun = this.getTips(this.tipregexFun, ele);
20196 var all = $.extend({}, tips, fun);
20197 if (Object.keys(all).length > 0) {
20198 this.attachTipStatuses(all);
20199 this.attachReceiverStatus(this.getTips(/(?:)/, ele));
20203 /** Return true if the comment node matches the regex. */
20204 commentMatches: function(regex, $e) {
20205 return $e.find('.md:first, .title:first').children().is(function() {
20206 return regex.test($(this).text());
20210 /** Find all things matching a regex. */
20211 getTips: function getComments(regex, ele) {
20213 var items = $(ele);
20214 if (items.is('.entry')) {
20215 items = items.closest('div.comment, div.self, div.link');
20217 items = items.find('div.comment, div.self, div.link');
20220 items.each(function() {
20221 var $this = $(this);
20222 if (module.commentMatches(regex, $this)) {
20223 var id = $this.attr('data-fullname');
20224 tips[id.replace(/^t._/, '')] = $this;
20230 attachTipStatuses: function attachTipStatuses(tips) {
20231 var iconStyle = 'vertical-align: text-bottom; margin-left: 8px;';
20232 var icons = this.icons;
20233 var tipIDs = Object.keys(tips);
20234 $.getJSON(this.api.gettips, {
20235 tips: tipIDs.toString()
20236 }, function(response) {
20237 var lastEvaluated = new Date(response.last_evaluated * 1000);
20238 response.tips.forEach(function (tip) {
20239 var id = tip.fullname.replace(/^t._/, '');
20240 var tagline = tips[id].find('.tagline').first();
20241 var icon = $('<a/>').attr({href: tip.tx, target: '_blank'});
20242 tagline.append(icon.append($('<img/>').attr({
20243 src: icons[tip.status],
20245 title: this.quantityString(tip) + ' → ' + tip.receiver +
20246 ' (' + tip.status + ')'
20248 tips[id].attr('id', 't1_' + id); // for later linking
20252 /* Deal with unanswered tips. */
20253 for (var id in tips) {
20254 if (this.commentMatches(this.tipregexFun, tips[id])) {
20255 continue; // probably wasn't actually a tip
20257 var date = tips[id].find('.tagline time:first')
20259 if (new Date(date) < lastEvaluated) {
20260 var tagline = tips[id].find('.tagline:first');
20261 tagline.append($('<img/>').attr({
20262 src: icons.cancelled,
20264 title: 'This tip is invalid.'
20271 attachReceiverStatus: function attachReceiverStatus(things) {
20272 var iconStyle = 'vertical-align: text-bottom; margin-left: 8px;';
20273 var icons = this.icons;
20274 var thingIDs = Object.keys(things);
20275 $.getJSON(this.api.gettipped, {
20276 tipped: thingIDs.toString()
20277 }, function(response) {
20278 response.forEach(function (tipped) {
20279 var id = tipped.fullname.replace(/^t._/, '');
20280 var thing = things[id];
20281 var tagline = thing.find('.tagline').first();
20282 var plural = tipped.tipQTY > 1;
20283 var title = this.quantityString(tipped) + ' to ' +
20284 thing.find('.author:first').text() + ' for this ';
20286 title = 'redditors have given ' + title;
20288 title = 'a redditor has given ' + title;
20290 if (thing.closest('.link').length === 0) {
20291 title += 'comment.';
20293 title += 'submission.';
20295 var icon = $('<img/>').attr({
20300 tagline.append(icon);
20302 tagline.append($('<span/>').text('x' + tipped.tipQTY));
20308 injectBotStatus: function injectBotStatus() {
20309 $.getJSON(this.api.gettips, function(response) {
20310 var lastEvaluated = new Date(response.last_evaluated * 1000);
20311 var botStatus = null;
20312 if (Date.now() - lastEvaluated > this.botDownThreshold) {
20313 botStatus = '<span class="status-down">DOWN</span>';
20315 botStatus = '<span class="status-up">UP</span>';
20317 $('.side a[href="http://bitcointip.net/status.php"]').html(botStatus);
20322 modules['troubleShooter'] = {
20323 moduleID: 'troubleShooter',
20324 moduleName: 'Troubleshooter',
20325 category: 'Troubleshoot',
20327 clearUserInfoCache: {
20331 description: 'Reset the <code>userInfo</code> cache for the currently logged in user. Useful for when link/comment karma appears to have frozen.'
20337 description: 'Reset the \'My Subreddits\' dropdown contents in the event of old/duplicate/missing entries.'
20343 description: 'Remove all entries for users with +1 or -1 vote tallies (only non-tagged users).'
20349 description: 'Warning: This will remove all your RES settings, including tags, saved comments, filters etc!'
20352 description: 'Resolve common problems and clean/clear unwanted settings data.' + '<br/><br/>' +
20353 'Your first line of defence against browser crashes/updates, or potential issues' +
20354 ' with RES, is a frequent backup.' + '<br/><br/>' +
20355 'See <a href="http://www.reddit.com/r/Enhancement/wiki/where_is_res_data_stored">here</a>' +
20356 ' for the location of the RES settings file for your browser/OS.',
20357 isEnabled: function () {
20358 return RESConsole.getModulePrefs(this.moduleID);
20361 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
20363 isMatchURL: function () {
20364 return RESUtils.isMatchURL(this.moduleID);
20366 beforeLoad: function () {
20368 css += 'body:not(.loggedin) #clearUserInfoCache ~ .optionDescription:before, body:not(.loggedin) #clearSubreddits ~ .optionDescription:before';
20369 css += '{content: "Functionality only for logged in users - ";color:#f00;font-weight:bold}';
20370 RESUtils.addCSS(css);
20373 this.options['clearUserInfoCache'].callback = modules['troubleShooter'].clearUICache;
20374 this.options['clearSubreddits'].callback = modules['troubleShooter'].clearSubreddits;
20375 this.options['clearTags'].callback = modules['troubleShooter'].clearTags;
20376 this.options['resetToFactory'].callback = modules['troubleShooter'].resetToFactory;
20378 clearUICache: function () {
20379 var user = RESUtils.loggedInUser();
20381 RESStorage.removeItem('RESUtils.userInfoCache.' + user);
20382 RESUtils.notification('Cached info for ' + user + ' was reset.', 2500);
20384 RESUtils.notification('You must be logged in to perform this task.', 2500);
20387 clearSubreddits: function () {
20388 var user = RESUtils.loggedInUser();
20390 RESStorage.removeItem('RESmodules.subredditManager.subreddits.' + user);
20391 RESUtils.notification('Subreddits for ' + user + ' were reset.', 2500);
20393 RESUtils.notification('You must be logged in to perform this task.', 2500);
20396 clearTags: function () {
20397 var confirm = window.confirm('Are you positive?');
20401 tags = RESStorage.getItem('RESmodules.userTagger.tags');
20403 tags = JSON.parse(tags);
20405 if ((tags[i].votes === 1 || tags[i].votes === -1) && !tags[i].hasOwnProperty('tag')) {
20410 tags = JSON.stringify(tags);
20411 RESStorage.setItem('RESmodules.userTagger.tags', tags);
20412 RESUtils.notification(cnt + ' entries removed.', 2500);
20415 RESUtils.notification('No action was taken', 2500);
20418 resetToFactory: function () {
20419 var confirm = window.confirm('This will kill all your settings and saved data. Are you sure?');
20421 for (var key in RESStorage) {
20422 if (key.indexOf('RES') !== -1) {
20423 RESStorage.removeItem(key);
20426 RESUtils.notification('All settings reset.', 2500);
20428 RESUtils.notification('No action was taken', 2500);
20438 * :: Now with support for touch events and multiple instances for
20439 * :: those situations that call for multiple easter eggs!
20440 * Code: http://konami-js.googlecode.com/
20441 * Examples: http://www.snaptortoise.com/konami-js
20442 * Copyright (c) 2009 George Mandis (georgemandis.com, snaptortoise.com)
20443 * Version: 1.3.2 (7/02/2010)
20444 * Licensed under the GNU General Public License v3
20445 * http://www.gnu.org/copyleft/gpl.html
20446 * Tested in: Safari 4+, Google Chrome 4+, Firefox 3+, IE7+ and Mobile Safari 2.2.1
20448 var Konami = function () {
20450 addEvent: function (obj, type, fn, ref_obj) {
20451 if (obj.addEventListener)
20452 obj.addEventListener(type, fn, false);
20453 else if (obj.attachEvent) {
20455 obj["e" + type + fn] = fn;
20456 obj[type + fn] = function () {
20457 obj["e" + type + fn](window.event, ref_obj);
20460 obj.attachEvent("on" + type, obj[type + fn]);
20464 prepattern: "38384040373937396665",
20465 almostThere: false,
20466 pattern: "3838404037393739666513",
20467 load: function (link) {
20468 this.addEvent(document, "keydown", function (e, ref_obj) {
20469 if (ref_obj) konami = ref_obj; // IE
20470 konami.input += e ? e.keyCode : event.keyCode;
20471 if (konami.input.length > konami.pattern.length) konami.input = konami.input.substr((konami.input.length - konami.pattern.length));
20472 if (konami.input == konami.pattern) {
20476 } else if ((konami.input == konami.prepattern) || (konami.input.substr(2, konami.input.length) == konami.prepattern)) {
20477 konami.almostThere = true;
20478 setTimeout(function () {
20479 konami.almostThere = false;
20483 this.iphone.load(link);
20485 code: function (link) {
20486 window.location = link;
20496 keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP", "TAP"],
20497 code: function (link) {
20500 load: function (link) {
20501 this.orig_keys = this.keys;
20502 konami.addEvent(document, "touchmove", function (e) {
20503 if (e.touches.length === 1 && konami.iphone.capture == true) {
20504 var touch = e.touches[0];
20505 konami.iphone.stop_x = touch.pageX;
20506 konami.iphone.stop_y = touch.pageY;
20507 konami.iphone.tap = false;
20508 konami.iphone.capture = false;
20509 konami.iphone.check_direction();
20512 konami.addEvent(document, "touchend", function (evt) {
20513 if (konami.iphone.tap == true) konami.iphone.check_direction(link);
20515 konami.addEvent(document, "touchstart", function (evt) {
20516 konami.iphone.start_x = evt.changedTouches[0].pageX;
20517 konami.iphone.start_y = evt.changedTouches[0].pageY;
20518 konami.iphone.tap = true;
20519 konami.iphone.capture = true;
20522 check_direction: function (link) {
20523 x_magnitude = Math.abs(this.start_x - this.stop_x);
20524 y_magnitude = Math.abs(this.start_y - this.stop_y);
20525 x = ((this.start_x - this.stop_x) < 0) ? "RIGHT" : "LEFT";
20526 y = ((this.start_y - this.stop_y) < 0) ? "DOWN" : "UP";
20527 result = (x_magnitude > y_magnitude) ? x : y;
20528 result = (this.tap == true) ? "TAP" : result;
20530 if (result == this.keys[0]) this.keys = this.keys.slice(1, this.keys.length);
20531 if (this.keys.length === 0) {
20532 this.keys = this.orig_keys;
20541 var _beforeLoadComplete = false;
20542 function RESdoBeforeLoad() {
20543 if (_beforeLoadComplete) return;
20544 _beforeLoadComplete = true;
20545 // if (beforeLoadDoneOnce) return;
20546 // first, go through each module and set all of the options so that if a module needs to check another module's options, they're ready...
20547 // console.log('get options start: ' + Date());
20548 for (var thisModuleID in modules) {
20549 if (typeof modules[thisModuleID] === 'object') {
20550 RESUtils.getOptions(thisModuleID);
20553 // console.log('get options end: ' + Date());
20554 for (var thisModuleID in modules) {
20555 if (typeof modules[thisModuleID] === 'object') {
20556 if (typeof modules[thisModuleID].beforeLoad === 'function') modules[thisModuleID].beforeLoad();
20560 GM_addStyle(RESUtils.css);
20561 // clear out css cache...
20565 function RESInit() {
20566 // $.browser shim since jQuery removed it
20568 safari: BrowserDetect.isSafari(),
20569 mozilla: BrowserDetect.isFirefox(),
20570 chrome: BrowserDetect.isChrome(),
20571 opera: BrowserDetect.isOpera()
20573 $.fn.safeHtml = function(string) {
20574 if (!string) return '';
20575 else return $(this).html(RESUtils.sanitizeHTML(string));
20578 RESUtils.initObservers();
20579 localStorageFail = false;
20582 $.extend(backup, RESStorage);
20583 delete backup.getItem;
20584 delete backup.setItem;
20585 delete backup.removeItem;
20586 console.log(backup);
20589 // Check for localStorage functionality...
20591 localStorage.setItem('RES.localStorageTest','test');
20592 // if this is a firefox addon, check for the old lsTest to see if they used to use the Greasemonkey script...
20593 // if so, present them with a notification explaining that they should download a new script so they can
20594 // copy their old settings...
20595 if (BrowserDetect.isFirefox()) {
20596 if ((localStorage.getItem('RES.lsTest') === 'test') && (localStorage.getItem('copyComplete') !== 'true')) {
20597 RESUtils.notification('<h2>Important Alert for Greasemonkey Users!</h2>Hey! It looks like you have upgraded to RES 4.0, but used to use the Greasemonkey version of RES. You\'re going to see double until you uninstall the Greasemonkey script. However, you should first copy your settings by clicking the blue button. <b>After installing, refresh this page!</b> <a target="_blank" class="RESNotificationButtonBlue" href="http://redditenhancementsuite.com/gmutil/reddit_enhancement_suite.user.js">GM->FF Import Tool</a>', 15000);
20598 localStorage.removeItem('RES.lsTest');
20600 // this is the only "old school" DOMNodeInserted event left... note to readers of this source code:
20601 // it will ONLY ever be added to the DOM in the specific instance of former OLD RES users from Greasemonkey
20602 // who haven't yet had the chance to copy their settings to the XPI version of RES. Once they've completed
20603 // that, this eventlistener will never be added again, nor will it be added for those who are not in this
20604 // odd/small subset of people.
20605 document.body.addEventListener('DOMNodeInserted', function(event) {
20606 if ((event.target.tagName === 'DIV') && (event.target.getAttribute('id') && event.target.getAttribute('id').indexOf('copyToSimpleStorage') !== -1)) {
20613 localStorageFail = true;
20616 document.body.classList.add('res','res-v430');
20618 if (localStorageFail) {
20619 RESFail = "Sorry, but localStorage seems inaccessible. Reddit Enhancement Suite can't work without it. \n\n";
20620 if (BrowserDetect.isSafari()) {
20621 RESFail += 'Since you\'re using Safari, it might be that you\'re in private browsing mode, which unfortunately is incompatible with RES until Safari provides a way to allow extensions localStorage access.';
20622 } else if (BrowserDetect.isChrome()) {
20623 RESFail += 'Since you\'re using Chrome, you might just need to go to your extensions settings and check the "Allow in Incognito" box.';
20624 } else if (BrowserDetect.isOpera()) {
20625 RESFail += 'Since you\'re using Opera, you might just need to go to your extensions settings and click the gear icon, then click "privacy" and check the box that says "allow interaction with private tabs".';
20627 RESFail += 'Since it looks like you\'re using Firefox, you probably need to go to about:config and ensure that dom.storage.enabled is set to true, and that dom.storage.default_quota is set to a number above zero (i.e. 5120, the normal default)".';
20629 var userMenu = document.querySelector('#header-bottom-right');
20631 var preferencesUL = userMenu.querySelector('UL');
20632 var separator = document.createElement('span');
20633 separator.setAttribute('class','separator');
20634 separator.textContent = '|';
20635 RESPrefsLink = document.createElement('a');
20636 RESPrefsLink.setAttribute('href','javascript:void(0)');
20637 RESPrefsLink.addEventListener('click', function(e) {
20638 e.preventDefault();
20641 RESPrefsLink.textContent = '[RES - ERROR]';
20642 RESPrefsLink.setAttribute('style','color: red; font-weight: bold;');
20643 insertAfter(preferencesUL, RESPrefsLink);
20644 insertAfter(preferencesUL, separator);
20647 document.body.addEventListener('mousemove', RESUtils.setMouseXY, false);
20648 // added this if statement because some people's Greasemonkey "include" lines are getting borked or ignored, so they're calling RES on non-reddit pages.
20649 if (location.href.match(/^https?:\/\/([\w]+\.)?reddit\.com/i)) {
20650 RESUtils.firstRun();
20651 RESUtils.checkForUpdate();
20652 // add the config console link...
20653 RESConsole.create();
20654 RESConsole.addConsoleLink();
20655 RESConsole.addConsoleDropdown();
20656 RESUtils.checkIfSubmitting();
20657 // go through each module and run it
20658 for (var thisModuleID in modules) {
20659 if (typeof modules[thisModuleID] === 'object') {
20660 // console.log(thisModuleID + ' start: ' + Date());
20661 // perfTest(thisModuleID+' start');
20662 modules[thisModuleID].go();
20663 // perfTest(thisModuleID+' end');
20664 // console.log(thisModuleID + ' end: ' + Date());
20667 GM_addStyle(RESUtils.css);
20668 // console.log('end: ' + Date());
20670 if ((location.href.match(/reddit\.honestbleeps\.com\/download/)) || (location.href.match(/redditenhancementsuite\.com\/download/))) {
20671 var installLinks = document.body.querySelectorAll('.install');
20672 for (var i=0, len=installLinks.length;i<len;i++) {
20673 installLinks[i].classList.add('update');
20674 installLinks[i].classList.add('res4'); // if update but not RES 4, then FF users == greasemonkey...
20675 installLinks[i].classList.remove('install');
20678 konami = new Konami();
20679 konami.code = function() {
20680 var baconBit = createElementWithID('div','baconBit');
20681 document.body.appendChild(baconBit);
20682 RESUtils.notification({header: 'RES Easter Eggies!', message: 'Mmm, bacon!'});
20683 setTimeout(function() {
20684 baconBit.classList.add('makeitrain');
20691 RESUtils.postLoad = true;
20695 function setUpRESStorage (response) {
20696 if (BrowserDetect.isChrome()) {
20697 RESStorage = response;
20698 // we'll set up a method for getItem, but it's not adviseable to use since it's asynchronous...
20699 RESStorage.getItem = function(key) {
20700 if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
20703 // if the fromBG parameter is true, we've been informed by another tab that this item has updated. We should update the data locally, but not send a background request.
20704 RESStorage.setItem = function(key, value, fromBG) {
20705 //Protect from excessive disk I/O...
20706 if (RESStorage[key] != value) {
20707 // save it locally in the RESStorage variable, but also write it to the extension's localStorage...
20708 // It's OK that saving it is asynchronous since we're saving it in this local variable, too...
20709 RESStorage[key] = value;
20711 requestType: 'localStorage',
20712 operation: 'setItem',
20717 chrome.extension.sendMessage(thisJSON);
20721 RESStorage.removeItem = function(key) {
20722 // delete it locally in the RESStorage variable, but also delete it from the extension's localStorage...
20723 // It's OK that deleting it is asynchronous since we're deleting it in this local variable, too...
20724 delete RESStorage[key];
20726 requestType: 'localStorage',
20727 operation: 'removeItem',
20730 chrome.extension.sendMessage(thisJSON);
20732 window.localStorage = RESStorage;
20734 } else if (BrowserDetect.isSafari()) {
20735 RESStorage = response;
20736 RESStorage.getItem = function(key) {
20737 if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
20740 RESStorage.setItem = function(key, value, fromBG) {
20741 //Protect from excessive disk I/O...
20742 if (RESStorage[key] != value) {
20743 // save it locally in the RESStorage variable, but also write it to the extension's localStorage...
20744 // It's OK that saving it is asynchronous since we're saving it in this local variable, too...
20745 RESStorage[key] = value;
20747 requestType: 'localStorage',
20748 operation: 'setItem',
20753 safari.self.tab.dispatchMessage("localStorage", thisJSON);
20757 RESStorage.removeItem = function(key) {
20758 // delete it locally in the RESStorage variable, but also delete it from the extension's localStorage...
20759 // It's OK that deleting it is asynchronous since we're deleting it in this local variable, too...
20760 delete RESStorage[key];
20762 requestType: 'localStorage',
20763 operation: 'removeItem',
20766 safari.self.tab.dispatchMessage("localStorage", thisJSON);
20768 window.localStorage = RESStorage;
20769 } else if (BrowserDetect.isOpera()) {
20770 RESStorage = response;
20771 RESStorage.getItem = function(key) {
20772 if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
20775 RESStorage.setItem = function(key, value, fromBG) {
20776 //Protect from excessive disk I/O...
20777 if (RESStorage[key] != value) {
20778 // save it locally in the RESStorage variable, but also write it to the extension's localStorage...
20779 // It's OK that saving it is asynchronous since we're saving it in this local variable, too...
20780 RESStorage[key] = value;
20782 requestType: 'localStorage',
20783 operation: 'setItem',
20788 opera.extension.postMessage(JSON.stringify(thisJSON));
20792 RESStorage.removeItem = function(key) {
20793 // delete it locally in the RESStorage variable, but also delete it from the extension's localStorage...
20794 // It's OK that deleting it is asynchronous since we're deleting it in this local variable, too...
20795 delete RESStorage[key];
20797 requestType: 'localStorage',
20798 operation: 'removeItem',
20801 opera.extension.postMessage(JSON.stringify(thisJSON));
20803 window.localStorage = RESStorage;
20804 } else if (BrowserDetect.isFirefox()) {
20805 RESStorage = response;
20806 RESStorage.getItem = function(key) {
20807 if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
20810 RESStorage.setItem = function(key, value, fromBG) {
20811 // save it locally in the RESStorage variable, but also write it to the extension's localStorage...
20812 // It's OK that saving it is asynchronous since we're saving it in this local variable, too...
20813 if (RESStorage[key] != value) {
20814 RESStorage[key] = value;
20816 requestType: 'localStorage',
20817 operation: 'setItem',
20822 self.postMessage(thisJSON);
20826 RESStorage.removeItem = function(key) {
20827 // delete it locally in the RESStorage variable, but also delete it from the extension's localStorage...
20828 // It's OK that deleting it is asynchronous since we're deleting it in this local variable, too...
20829 delete RESStorage[key];
20831 requestType: 'localStorage',
20832 operation: 'removeItem',
20835 self.postMessage(thisJSON);
20837 window.localStorage = RESStorage;
20839 // must be firefox w/greasemonkey...
20841 RESStorage.getItem = function(key) {
20842 if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
20843 RESStorage[key] = GM_getValue(key);
20844 if (typeof RESStorage[key] === 'undefined') return null;
20845 return GM_getValue(key);
20847 RESStorage.setItem = function(key, value) {
20848 // save it locally in the RESStorage variable, but also write it to the extension's localStorage...
20849 // It's OK that saving it is asynchronous since we're saving it in this local variable, too...
20850 // Wow, GM_setValue doesn't support big integers, so we have to store anything > 2147483647 as a string, so dumb.
20851 if (typeof value !== 'undefined') {
20852 // if ((typeof value === 'number') && (value > 2147483647)) {
20853 if (typeof value === 'number') {
20854 value = value.toString();
20856 //Protect from excessive disk I/O...
20857 if (RESStorage[key] != value) {
20858 RESStorage[key] = value;
20859 // because we may want to use jQuery events to call GM_setValue and GM_getValue, we must use this ugly setTimeout hack.
20860 setTimeout(function() {
20861 GM_setValue(key, value);
20867 RESStorage.removeItem = function(key) {
20868 // delete it locally in the RESStorage variable, but also delete it from the extension's localStorage...
20869 // It's OK that deleting it is asynchronous since we're deleting it in this local variable, too...
20870 delete RESStorage[key];
20871 GM_deleteValue(key);
20879 // Don't fire the script on the iframe. This annoyingly fires this whole thing twice. Yuck.
20880 // Also don't fire it on static.reddit or thumbs.reddit, as those are just images.
20881 // Also omit blog and code.reddit
20882 if ((typeof RESRunOnce !== 'undefined') || (location.href.match(/\/toolbar\/toolbar\?id/i)) || (location.href.match(/comscore-iframe/i)) || (location.href.match(/static\.reddit/i)) || (location.href.match(/thumbs\.reddit/i)) || (location.href.match(/blog\.reddit/i)) || (location.href.match(/code\.reddit/i)) || (location.href.match(/metareddit\.com/i))) {
20887 // call preInit function - work in this function should be kept minimal. It's for
20888 // doing stuff as early as possible prior to pageload, and even prior to the localStorage copy
20889 // from the background.
20890 // Specifically, this is used to add a class to the document for .res-nightmode, etc, as early
20891 // as possible to avoid the flash of unstyled content.
20892 RESUtils.preInit();
20895 if (BrowserDetect.isChrome()) {
20896 // we've got chrome, get a copy of the background page's localStorage first, so don't init until after.
20898 requestType: 'getLocalStorage'
20900 chrome.extension.sendMessage(thisJSON, function(response) {
20901 // Does RESStorage have actual data in it? If it doesn't, they're a legacy user, we need to copy
20902 // old school localStorage from the foreground page to the background page to keep their settings...
20903 if (typeof response.importedFromForeground === 'undefined') {
20904 // it doesn't exist.. copy it over...
20906 requestType: 'saveLocalStorage',
20909 chrome.extension.sendMessage(thisJSON, function(response) {
20910 setUpRESStorage(response);
20913 setUpRESStorage(response);
20916 } else if (BrowserDetect.isSafari()) {
20917 // we've got safari, get localStorage from background process
20919 requestType: 'getLocalStorage'
20921 safari.self.tab.dispatchMessage("getLocalStorage", thisJSON);
20922 } else if (BrowserDetect.isFirefox()) {
20923 // we've got firefox jetpack, get localStorage from background process
20925 requestType: 'getLocalStorage'
20927 self.postMessage(thisJSON);
20928 } else if (BrowserDetect.isOpera()) {
20929 // I freaking hate having to use different code that won't run in other browsers to log debugs, so I'm overriding console.log with opera.postError here
20930 // so I don't have to litter my code with different statements for different browsers when debugging.
20931 console.log = opera.postError;
20932 opera.extension.addEventListener( "message", operaMessageHandler, false);
20933 window.addEventListener("DOMContentLoaded", function(u) {
20934 // we've got opera, let's check for old localStorage...
20935 // RESInit() will be called from operaMessageHandler()
20937 requestType: 'getLocalStorage'
20939 opera.extension.postMessage(JSON.stringify(thisJSON));
20942 // Check if GM_getValue('importedFromForeground') has been set.. if not, this is an old user using localStorage;
20943 (typeof unsafeWindow !== 'undefined') ? ls = unsafeWindow.localStorage : ls = localStorage;
20944 if (GM_getValue('importedFromForeground') !== 'true') {
20945 // It doesn't exist, so we need to copy localStorage over to GM_setValue storage...
20946 for (var i = 0, len=ls.length; i < len; i++){
20947 var value = ls.getItem(ls.key(i));
20948 if (typeof value !== 'undefined') {
20949 if ((typeof value === 'number') && (value > 2147483647)) {
20950 value = value.toString();
20953 GM_setValue(ls.key(i), value);
20957 GM_setValue('importedFromForeground','true');
20961 // console.log(GM_listValues());
20965 function RESInitReadyCheck() {
20966 if ((typeof RESStorage.getItem !== 'function') || (typeof document.body === 'undefined') || (document.body === null)) {
20967 setTimeout(RESInitReadyCheck, 50);
20969 if (BrowserDetect.isFirefox()) {
20970 // firefox addon sdk... we've included jQuery...
20971 // also, for efficiency, we're going to try using unsafeWindow for "less secure" (but we're not going 2 ways here, so that's OK) but faster DOM node access...
20972 document = unsafeWindow.document;
20973 window = unsafeWindow;
20974 if (typeof $ !== 'function') {
20975 console.log('Uh oh, something has gone wrong loading jQuery...');
20977 } else if ((typeof unsafeWindow !== 'undefined') && (unsafeWindow.jQuery)) {
20978 // greasemonkey -- should load jquery automatically because of @require line
20979 // in this file's header
20980 if (typeof $ === 'undefined') {
20981 // greasemonkey-like userscript
20982 $ = unsafeWindow.jQuery;
20985 } else if (typeof window.jQuery === 'function') {
20990 // chrome and safari...
20991 if (typeof $ !== 'function') {
20992 console.log('Uh oh, something has gone wrong loading jQuery...');
20995 if (BrowserDetect.isSafari()) {
20996 // since safari's built in extension stylesheets are treated as user stylesheets,
20997 // we can't inject them that way. That makes them "user stylesheets" which would make
20998 // them require !important everywhere - we don't want that, so we'll inject this way instead.
20999 var loadCSS = function(filename) {
21000 var linkTag = document.createElement('link');
21001 linkTag.setAttribute('rel','stylesheet');
21002 linkTag.href = safari.extension.baseURI+filename;
21003 document.head.appendChild(linkTag);
21006 // include CSS files, then load scripts.
21007 var cssFiles = ['res.css', 'commentBoxes.css', 'nightmode.css'];
21008 for (var i in cssFiles) {
21009 loadCSS(cssFiles[i]);
21013 if (BrowserDetect.isOpera()) {
21014 // require.js-like modular injected scripts, code via:
21015 // http://my.opera.com/BS-Harou/blog/2012/08/08/modular-injcted-scripts-in-extensions
21016 // Note: This code requires Opera 12.50 to run!
21017 if (typeof opera.extension.getFile === 'function') {
21018 var loadCSS = function(filename) {
21019 var fileObj = opera.extension.getFile(filename);
21021 // Read out the File object as a Data URI:
21022 var fr = new FileReader();
21023 fr.onload = function() {
21024 // Load the library
21025 var styleTag = document.createElement("style");
21026 styleTag.textContent = fr.result;
21027 document.body.appendChild(styleTag);
21029 fr.readAsText(fileObj);
21033 // include CSS files, then load scripts.
21034 var cssFiles = ['res.css', 'commentBoxes.css', 'nightmode.css'];
21035 for (var i in cssFiles) {
21036 loadCSS(cssFiles[i]);
21039 (function(){var e=opera.extension,t={text:"readAsText",json:"readAsText",dataurl:"readAsDataURL",arraybuffer:"readAsArrayBuffer"};"getFile"in e&&!("getFileData"in e)&&(e.getFileData=function(e,n,r){typeof n==="function"?(r=n,n="text"):(n=n&&n.toLowerCase(),n=n in t?n:"text");if(typeof r!=="function")return;var i=opera.extension.getFile(e);if(i){var s=new FileReader;s.onload=function(e){if(n=="json")try{r(JSON.parse(s.result),i)}catch(e){r(null)}else r(s.result,i)},s.onerror=function(e){r(null)},s[t[n]](i)}else setTimeout(r,0,null,i)})})();var global=this,require=function(){function define(e,t){typeof e==="function"||typeof t!=="function"?define.compiled=typeof e==="undefined"?null:e:(define._wait=!0,require(e,function(e){var n=[].slice.call(arguments,1),r=e.pop();r.cb(t.apply(global,n))}.bind(global,define._store)))}return define.compiled=null,define._store=null,define._wait=!1,function(){function _compile(){define.compiled=null,define._store=arguments[1]._store;with({})eval(arguments[0]);return define._wait?(define._wait=!1,arguments[1]._store.push({cb:arguments[3],path:arguments[4]}),!1):(processData(define.compiled,arguments[1],arguments[2]),!0)}function processData(e,t,n){t.temp[n]&&delete t.temp[n],t.add(e,n);var r=t.path;if(!(r[n]in require._cache)||e&&require._cache[r[n]]===null)require._cache[r[n]]=e;var i=t.temp[n+1];if(i)if(!i.parsedPath[1]){var s=_compile(i.data,t,n+1,i.cb,i.parsedPath[0]);s||(require._cache[i.path]=null)}else processData(i.data,t,n+1)}function wait(e,t){setTimeout(function(){t.apply(global,e)},0)}function compileCB(e,t,n,r){processData(r,t,n),t.length==t.path.length&&e&&wait(t,e)}function parsePath(e){var t=e.split("!");return!t[1]&&e.indexOf(/\.js$/i)===-1&&(t[0]=e+=".js"),t}return function(e,t){var n=[];n.temp=[],n._store=[],n.add=function(r,i){return this.length==i?(this.push(r),!0):(this.temp[i]={data:r,cb:compileCB.bind(global,t,n,i),parsedPath:parsePath(e[i]),path:e[i]},!1)};if(!e.length)return wait(n,t),null;Array.isArray(e)||(e=[e]),n.path=e;for(var r=0,i=e.length;r<i;r++){if(e[r]==="!domReady"){document.readyState==="complete"||document.readyState==="interactive"?processData(document,n,r):document.addEventListener("DOMContentLoaded",function(r){processData(document,n,r),n.length==e.length&&t&&wait(n,t)}.bind(global,r));continue}var s=parsePath(e[r]);if(!s[0]){processData(null,n,r);continue}if(e[r]in require._cache){processData(require._cache[e[r]],n,r);continue}opera.extension.getFileData((require._base||"")+s[0],s[1]||"text",function(r,i,s){if(s)if(!i[1]){if(n.length!=r){if(n.length>r){debugger;alert("oh shit, this shoud not happen!")}processData(s,n,r);return}var o=_compile(s,n,r,compileCB.bind(global,t,n,r),i[0]);if(!o){require._cache[e[r]]=null;return}}else processData(s,n,r);else processData(null,n,r);n.length==e.length&&t&&wait(n,t)}.bind(global,r,s))}if(n.length==e.length)return t&&wait(n,t),n.length==1?n[0]:n}}()}();require._cache={},require._base="/modules/";
21041 // save Reddit's jQuery, because this script is going to jack it up.
21042 // now, take the new jQuery in and store it local to RES's scope (it's a var up top)
21043 var redditJq = window.$;
21044 require(['jquery-1.9.1.min', 'guiders-1.2.8', /*'tinycon',*/ 'snuownd', 'jquery.dragsort-0.6', 'jquery.tokeninput', 'jquery-fieldselection.min'], function() {
21046 jQuery = window.jQuery;
21047 guiders = window.guiders;
21048 //Tinycon = window.Tinycon;
21049 SnuOwnd = window.SnuOwnd;
21050 // now, return the window.$ / window.jQuery back to its original state.
21051 window.$ = redditJq;
21052 window.jQuery = redditJq;
21059 $(document).ready(RESInit);
21064 window.onload = RESInitReadyCheck();
21067 function perfTest(name) {
21068 var d = new Date();
21069 var diff = d.getTime() - lastPerf;
21070 console.log(name+' executed. Diff since last: ' + diff +'ms');
21071 lastPerf=d.getTime();