]> git.rmz.io Git - dotfiles.git/blob - dwb/greasemonkey/reddit_enhancement_suite.user.js.disable
add awesome website
[dotfiles.git] / dwb / greasemonkey / reddit_enhancement_suite.user.js.disable
1 // ==UserScript==
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/*
14 // @version 4.3.0.4
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
18 // ==/UserScript==
19
20 /*jshint undef: true, unused: true, strict: false, laxbreak: true, multistr: true, smarttabs: true, sub: true, browser: true */
21
22 var RESVersion = "4.3.0.4";
23
24 var jQuery, $, guiders, Tinycon, SnuOwnd;
25
26 /*
27 Reddit Enhancement Suite - a suite of tools to enhance Reddit
28 Copyright (C) 2010-2012 - honestbleeps (steve@honestbleeps.com)
29
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):
31
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.
35
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.
40
41 I can't legally hold you to any of this - I'm just asking out of courtesy.
42
43 Thanks, I appreciate your consideration. Without further ado, the all-important GPL Statement:
44
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.
49
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.
54
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/>.
57
58 */
59
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;}';
76
77
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; }';
89 /**
90 * For optimization, the arrows image is inlined in the css below.
91 *
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.
94 */
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; }';
104
105
106
107 // DOM utility functions
108 var escapeLookups = { "&": "&amp;", '"': "&quot;", "<": "&lt;", ">": "&gt;" };
109 function escapeHTML(str) {
110 return (typeof str === 'undefined' || str === null) ?
111 null :
112 str.toString().replace(/[&"<>]/g, function(m) { return escapeLookups[m]; });
113 }
114
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);
121 } else {
122 referenceNode.parentNode.insertBefore( newNode, referenceNode.nextSibling );
123 }
124 }
125 }
126 function createElementWithID(elementType, id, classname) {
127 var obj = document.createElement(elementType);
128 if (id !== null) {
129 obj.setAttribute('id', id);
130 }
131 if ((typeof classname !== 'undefined') && (classname !== '')) {
132 obj.setAttribute('class', classname);
133 }
134 return obj;
135 }
136
137 // this alias is to account for opera having different behavior...
138 if (typeof navigator === 'undefined') navigator = window.navigator;
139
140 //Because Safari 5.1 doesn't have Function.bind
141 if (typeof Function.prototype.bind === 'undefined') {
142 Function.prototype.bind = function(context) {
143 var oldRef = this;
144 return function() {
145 return oldRef.apply(context || null, Array.prototype.slice.call(arguments));
146 };
147 };
148 }
149
150 var BrowserDetect = {
151 init: function () {
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";
157
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;
163
164 // null out MutationObserver to test legacy DOMNodeInserted
165 // this.MutationObserver = null;
166 },
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;
172 if (dataString) {
173 if (dataString.indexOf(data[i].subString) !== -1)
174 return data[i].identity;
175 }
176 else if (dataProp)
177 return data[i].identity;
178 }
179 },
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));
184 },
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'; },
190 dataBrowser: [
191 {
192 string: navigator.userAgent,
193 subString: "OPR/",
194 identity: "Opera"
195 },
196 {
197 string: navigator.userAgent,
198 subString: "Chrome",
199 identity: "Chrome"
200 },
201 { string: navigator.userAgent,
202 subString: "OmniWeb",
203 versionSearch: "OmniWeb/",
204 identity: "OmniWeb"
205 },
206 {
207 string: navigator.vendor,
208 subString: "Apple",
209 identity: "Safari",
210 versionSearch: "Version"
211 },
212 {
213 prop: window.opera,
214 identity: "Opera",
215 versionSearch: "Version"
216 },
217 {
218 string: navigator.vendor,
219 subString: "iCab",
220 identity: "iCab"
221 },
222 {
223 string: navigator.vendor,
224 subString: "KDE",
225 identity: "Konqueror"
226 },
227 {
228 string: navigator.userAgent,
229 subString: "Firefox",
230 identity: "Firefox"
231 },
232 {
233 string: navigator.vendor,
234 subString: "Camino",
235 identity: "Camino"
236 },
237 { // for newer Netscapes (6+)
238 string: navigator.userAgent,
239 subString: "Netscape",
240 identity: "Netscape"
241 },
242 {
243 string: navigator.userAgent,
244 subString: "MSIE",
245 identity: "Explorer",
246 versionSearch: "MSIE"
247 },
248 {
249 string: navigator.userAgent,
250 subString: "Gecko",
251 identity: "Mozilla",
252 versionSearch: "rv"
253 },
254 {
255 // for older Netscapes (4-)
256 string: navigator.userAgent,
257 subString: "Mozilla",
258 identity: "Netscape",
259 versionSearch: "Mozilla"
260 }
261 ],
262 dataOS : [
263 {
264 string: navigator.platform,
265 subString: "Win",
266 identity: "Windows"
267 },
268 {
269 string: navigator.platform,
270 subString: "Mac",
271 identity: "Mac"
272 },
273 {
274 string: navigator.userAgent,
275 subString: "iPhone",
276 identity: "iPhone/iPod"
277 },
278 {
279 string: navigator.platform,
280 subString: "Linux",
281 identity: "Linux"
282 }
283 ]
284
285 };
286 BrowserDetect.init();
287
288 var safeJSON = {
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) {
293 try {
294 if (BrowserDetect.isSafari()) {
295 if (data.substring(0,2) === 's{') {
296 data = data.substring(1,data.length);
297 }
298 }
299 return JSON.parse(data);
300 } catch (error) {
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);
309 });
310 } else {
311 alert('Error caught: JSON parse failure on the following data: ' + data);
312 }
313 return {};
314 }
315 }
316 };
317
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) {
324 toArr.push(false);
325 }
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;
330 }
331 if (fromArr[i] !== toArr[i]) return false;
332 }
333 return true;
334 }
335
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);
344 }
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;
350 else return true;
351 }
352
353 function operaUpdateCallback(obj) {
354 RESUtils.compareVersion(obj);
355 }
356 function operaForcedUpdateCallback(obj) {
357 RESUtils.compareVersion(obj, true);
358 }
359
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: [] };
363
364
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);
372 break;
373 case 'compareVersion':
374 var forceUpdate = false;
375 if (typeof msgEvent.message.forceUpdate !== 'undefined') forceUpdate = true;
376 RESUtils.compareVersion(msgEvent.message, forceUpdate);
377 break;
378 case 'loadTweet':
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');
384 break;
385 // for now, commenting out the old way of handling tweets as AMO will not approve.
386 /*
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');
404 */
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...
410 var thisJSON = {
411 requestType: 'saveLocalStorage',
412 data: localStorage
413 };
414 self.postMessage(thisJSON);
415 } else {
416 setUpRESStorage(msgEvent.message);
417 //RESInit();
418 }
419 break;
420 case 'saveLocalStorage':
421 // Okay, we just copied localStorage from foreground to background, let's set it up...
422 setUpRESStorage(msgEvent.message);
423 break;
424 case 'localStorage':
425 RESStorage.setItem(msgEvent.itemName, msgEvent.itemValue, true);
426 break;
427 default:
428 // console.log('unknown event type in self.on');
429 // console.log(msgEvent.toSource());
430 break;
431 }
432 });
433 }
434
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);
441 break;
442 case 'compareVersion':
443 var forceUpdate = false;
444 if (typeof msgEvent.message.forceUpdate !== 'undefined') forceUpdate = true;
445 RESUtils.compareVersion(msgEvent.message, forceUpdate);
446 break;
447 case 'loadTweet':
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');
453 break;
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...
459 var thisJSON = {
460 requestType: 'saveLocalStorage',
461 data: localStorage
462 };
463 safari.self.tab.dispatchMessage('saveLocalStorage', thisJSON);
464 } else {
465 setUpRESStorage(msgEvent.message);
466 //RESInit();
467 }
468 break;
469 case 'saveLocalStorage':
470 // Okay, we just copied localStorage from foreground to background, let's set it up...
471 setUpRESStorage(msgEvent.message);
472 //RESInit();
473 break;
474 case 'addURLToHistory':
475 var url = msgEvent.message.url;
476 modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
477 break;
478 case 'localStorage':
479 RESStorage.setItem(msgEvent.message.itemName, msgEvent.message.itemValue, true);
480 break;
481 default:
482 // console.log('unknown event type in safariMessageHandler');
483 break;
484 }
485 }
486
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);
494 break;
495 case 'compareVersion':
496 var forceUpdate = false;
497 if (typeof eventData.data.forceUpdate !== 'undefined') forceUpdate = true;
498 RESUtils.compareVersion(eventData.data, forceUpdate);
499 break;
500 case 'loadTweet':
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');
506 break;
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...
512 var thisJSON = {
513 requestType: 'saveLocalStorage',
514 data: localStorage
515 };
516 opera.extension.postMessage(JSON.stringify(thisJSON));
517 } else {
518 if (location.hostname.match('reddit')) {
519 setUpRESStorage(eventData.data);
520 //RESInit();
521 }
522 }
523 break;
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')) {
528 //RESInit();
529 }
530 break;
531 case 'localStorage':
532 if ((typeof RESStorage !== 'undefined') && (typeof RESStorage.setItem === 'function')) {
533 RESStorage.setItem(eventData.itemName, eventData.itemValue, true);
534 } else {
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);
539 } else {
540 setTimeout(function() { waitForRESStorage(eData); }, 200);
541 }
542 };
543 var savedEventData = {
544 itemName: eventData.itemName,
545 itemValue: eventData.itemValue
546 };
547 waitForRESStorage(savedEventData);
548 }
549 break;
550 case 'addURLToHistory':
551 var url = eventData.url;
552 if (! eventData.isPrivate) {
553 modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
554 }
555 break;
556 default:
557 // console.log('unknown event type in operaMessageHandler');
558 break;
559 }
560 }
561
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) {
567 case 'localStorage':
568 RESStorage.setItem(request.itemName, request.itemValue, true);
569 break;
570 default:
571 // sendResponse({status: "unrecognized request type"});
572 break;
573 }
574 }
575 );
576 }
577
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);
583 }
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();
585
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;
593 }
594
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') {
600 console = {
601 log: function(str) {
602 return false;
603 }
604 };
605 }
606 }
607
608
609
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];
619 if (head) {
620 head.appendChild(style);
621 }
622 };
623
624 GM_deleteValue = function(name) {
625 localStorage.removeItem(name);
626 };
627
628 GM_getValue = function(name, defaultValue) {
629 var value = localStorage.getItem(name);
630 if (!value)
631 return defaultValue;
632 var type = value[0];
633 value = value.substring(1);
634 switch (type) {
635 case 'b':
636 return value === 'true';
637 case 'n':
638 return Number(value);
639 default:
640 return value;
641 }
642 };
643
644 GM_log = function(message) {
645 console.log(message);
646 };
647
648 GM_registerMenuCommand = function(name, funk) {
649 //todo
650 };
651
652 GM_setValue = function(name, value) {
653 value = (typeof value)[0] + value;
654 localStorage.setItem(name, value);
655 };
656
657 if (BrowserDetect.browser === "Explorer") {
658 GM_xmlhttpRequest = function(obj) {
659 var request,
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);
668 return request;
669 } else {
670 request = new XMLHttpRequest();
671 request.onreadystatechange=function() {
672 if (obj.onreadystatechange) {
673 obj.onreadystatechange(request);
674 }
675 if (request.readyState === 4 && obj.onload) {
676 obj.onload(request);
677 }
678 };
679 request.onerror = function() {
680 if(obj.onerror) {
681 obj.onerror(request);
682 }
683 };
684 try {
685 request.open(obj.method,obj.url,true);
686 } catch(e) {
687 if(obj.onerror) {
688 obj.onerror({
689 readyState:4,
690 responseHeaders:'',
691 responseText:'',
692 responseXML:'',
693 status:403,
694 statusText:'Forbidden'
695 });
696 }
697 return;
698 }
699 if(obj.headers) {
700 for (var name in obj.headers) {
701 request.setRequestHeader(name,obj.headers[name]);
702 }
703 }
704 request.send(obj.data);
705 return request;
706 }
707 };
708 }
709 if (BrowserDetect.isChrome()) {
710 GM_xmlhttpRequest = function(obj) {
711 var crossDomain = (obj.url.indexOf(location.hostname) === -1);
712
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);
718 });
719 }
720 } else {
721 var request=new XMLHttpRequest();
722 request.onreadystatechange = function() {
723 if (obj.onreadystatechange) {
724 obj.onreadystatechange(request);
725 }
726 if(request.readyState === 4 && obj.onload) {
727 obj.onload(request);
728 }
729 };
730 request.onerror = function() {
731 if(obj.onerror) {
732 obj.onerror(request);
733 }
734 };
735 try {
736 request.open(obj.method,obj.url,true);
737 } catch(e) {
738 if (obj.onerror) {
739 obj.onerror({
740 readyState:4,
741 responseHeaders:'',
742 responseText:'',
743 responseXML:'',
744 status:403,
745 statusText:'Forbidden'
746 });
747 }
748 return;
749 }
750 if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
751 request.send(obj.data); return request;
752 }
753 };
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.
759
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);
764
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);
769 xhrQueue.count++;
770 } else {
771 var request=new XMLHttpRequest();
772 request.onreadystatechange = function() {
773 if (obj.onreadystatechange) {
774 obj.onreadystatechange(request);
775 }
776 if (request.readyState === 4 && obj.onload) {
777 obj.onload(request);
778 }
779 };
780 request.onerror = function() {
781 if(obj.onerror) {
782 obj.onerror(request);
783 }
784 };
785 try {
786 request.open(obj.method,obj.url,true);
787 } catch(e) {
788 if (obj.onerror) {
789 obj.onerror({
790 readyState:4,
791 responseHeaders:'',
792 responseText:'',
793 responseXML:'',
794 status:403,
795 statusText:'Forbidden'
796 });
797 }
798 return;
799 }
800 if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
801 request.send(obj.data); return request;
802 }
803 };
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...
808
809 // oy vey... cross domain same issue with Opera.
810 var crossDomain = (obj.url.indexOf(location.hostname) === -1);
811
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));
816 xhrQueue.count++;
817 } else {
818 var request=new XMLHttpRequest();
819 request.onreadystatechange = function() {
820 if (obj.onreadystatechange) {
821 obj.onreadystatechange(request);
822 }
823 if (request.readyState === 4 && obj.onload) {
824 obj.onload(request);
825 }
826 };
827 request.onerror = function() {
828 if(obj.onerror) {
829 obj.onerror(request);
830 }
831 };
832 try {
833 request.open(obj.method,obj.url,true);
834 } catch(e) {
835 if (obj.onerror) {
836 obj.onerror({
837 readyState:4,
838 responseHeaders:'',
839 responseText:'',
840 responseXML:'',
841 status:403,
842 statusText:'Forbidden'
843 });
844 }
845 return;
846 }
847 if (obj.headers) {
848 for (var name in obj.headers) {
849 request.setRequestHeader(name,obj.headers[name]);
850 }
851 }
852 request.send(obj.data); return request;
853 }
854 };
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);
859
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);
867 xhrQueue.count++;
868 }
869 } else {
870 var request=new XMLHttpRequest();
871 request.onreadystatechange = function() {
872 if (obj.onreadystatechange) {
873 obj.onreadystatechange(request);
874 }
875 if(request.readyState === 4 && obj.onload) {
876 obj.onload(request);
877 }
878 };
879 request.onerror = function() {
880 if(obj.onerror) {
881 obj.onerror(request);
882 }
883 };
884 try {
885 request.open(obj.method,obj.url,true);
886 } catch(e) {
887 if (obj.onerror) {
888 obj.onerror({
889 readyState:4,
890 responseHeaders:'',
891 responseText:'',
892 responseXML:'',
893 status:403,
894 statusText:'Forbidden'
895 });
896 }
897 return;
898 }
899 if(obj.headers) { for(var name in obj.headers) { request.setRequestHeader(name,obj.headers[name]); } }
900 request.send(obj.data); return request;
901 }
902 };
903 }
904 } else {
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() {
910 oldgmx(params);
911 }, 0);
912 };
913 }
914
915
916 var modules = {};
917
918 // define common RESUtils - reddit related functions and data that may need to be accessed...
919 var RESUtils = {
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();
924 },
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() {
928 if (document) {
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();
933 }
934 } else {
935 setTimeout(RESUtils.getDocHTML, 1);
936 }
937 },
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);
946 }
947 return randomString;
948 },
949 postLoad: false,
950 css: '',
951 addCSS: function(css) {
952 if (RESUtils.postLoad) {
953 var style = $('<style />').html(css).appendTo('head');
954 return {
955 remove: function() { style.remove(); }
956 };
957 } else {
958 this.css += css;
959 }
960 },
961 insertParam: function(href, key, value) {
962 var pre = '&';
963 if (href.indexOf('?') === -1) pre = '?';
964 return href + pre + key + '=' + value;
965 },
966 // checks if script should run on current URL using exclude / include.
967 isMatchURL: function (moduleID) {
968 var i=0;
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)) {
978 return false;
979 }
980 }
981 }
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)) {
986 return true;
987 }
988 }
989 return false;
990 },
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;
997 }
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') {
1006 newOption = true;
1007 storedOptions[attrname] = codeOptions[attrname];
1008 } else {
1009 codeOptions[attrname].value = storedOptions[attrname].value;
1010 }
1011 }
1012 modules[moduleID].options = codeOptions;
1013 if (newOption) {
1014 RESStorage.setItem('RESoptions.' + moduleID, JSON.stringify(modules[moduleID].options));
1015 }
1016 } else {
1017 // nothing in localStorage, let's set the defaults...
1018 RESStorage.setItem('RESoptions.' + moduleID, JSON.stringify(modules[moduleID].options));
1019 }
1020 this.getOptionsFirstRun[moduleID] = true;
1021 return modules[moduleID].options;
1022 },
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]);
1028 }
1029 return result;
1030 },
1031 setOption: function(moduleID, optionName, optionValue) {
1032 if (optionName.match(/_[\d]+$/)) {
1033 optionName = optionName.replace(/_[\d]+$/,'');
1034 }
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);
1043 } else {
1044 saveOptionValue = parseInt(optionValue, 10);
1045 }
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));
1051 return true;
1052 },
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);
1058 },
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);
1064 },
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;
1071 } else {
1072 if (tryingEarly) {
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;
1079 } else {
1080 this.loggedInUserCached = null;
1081 }
1082 }
1083 }
1084 return this.loggedInUserCached;
1085 },
1086 loggedInUserHash: function() {
1087 this.loggedInUser();
1088 return this.loggedInUserHashCached;
1089 },
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;
1094
1095 // Default to getting live data (i.e. from reddit's server)
1096 live = (typeof live === "boolean" ? live : true);
1097
1098 if (!(username in RESUtils.userInfoCallbacks)) {
1099 RESUtils.userInfoCallbacks[username] = [];
1100 }
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;
1110 GM_xmlhttpRequest({
1111 method: "GET",
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
1118 };
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);
1123 }
1124 RESUtils.userInfoRunning = false;
1125 }
1126 });
1127 }
1128 } else {
1129 while (RESUtils.userInfoCallbacks[username].length > 0) {
1130 var thisCallback = RESUtils.userInfoCallbacks[username].pop();
1131 thisCallback(userInfoCache.userInfo);
1132 }
1133 }
1134 },
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') {
1145 var pageType = '';
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)) {
1152 pageType = 'inbox';
1153 } else if (RESUtils.submitRegex.test(currURL)) {
1154 pageType = 'submit';
1155 } else if (RESUtils.prefsRegex.test(currURL)) {
1156 pageType = 'prefs';
1157 } else if (RESUtils.wikiRegex.test(currURL)) {
1158 pageType = 'wiki';
1159 } else {
1160 pageType = 'linklist';
1161 }
1162 this.pageTypeSaved = pageType;
1163 }
1164 return this.pageTypeSaved;
1165 },
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;
1172 } else {
1173 this.isCommentPermalinkSaved = false;
1174 }
1175 }
1176
1177 return this.isCommentPermalinkSaved;
1178 },
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());
1187 return match[1];
1188 } else {
1189 if (check) return false;
1190 return null;
1191 }
1192 } else {
1193 if (check) return (this.curSub.toLowerCase() === check.toLowerCase());
1194 return this.curSub;
1195 }
1196 },
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());
1203 return match[1];
1204 } else {
1205 if (check) return false;
1206 return null;
1207 }
1208 } else {
1209 if (check) return (this.curDom.toLowerCase() === check.toLowerCase());
1210 return this.curDom;
1211 }
1212 },
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];
1218 return match[1];
1219 } else {
1220 return null;
1221 }
1222 } else {
1223 return this.curUserProfile;
1224 }
1225 },
1226 getXYpos: function (obj) {
1227 var topValue= 0,leftValue= 0;
1228 while(obj) {
1229 leftValue += obj.offsetLeft;
1230 topValue += obj.offsetTop;
1231 obj = obj.offsetParent;
1232 }
1233 return { 'x': leftValue, 'y': topValue };
1234 },
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;
1246 }
1247 return (
1248 top >= window.pageYOffset &&
1249 left >= window.pageXOffset &&
1250 (top + height) <= (window.pageYOffset + window.innerHeight - headerOffset) &&
1251 (left + width) <= (window.pageXOffset + window.innerWidth)
1252 );
1253 },
1254 setMouseXY: function(e) {
1255 e = e || window.event;
1256 var cursor = {x:0, y:0};
1257 if (e.pageX || e.pageY) {
1258 cursor.x = e.pageX;
1259 cursor.y = e.pageY;
1260 } else {
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;
1269 }
1270 RESUtils.mouseX = cursor.x;
1271 RESUtils.mouseY = cursor.y;
1272 },
1273 elementUnderMouse: function ( obj ) {
1274 var $obj = $(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)) {
1282 return true;
1283 } else {
1284 return false;
1285 }
1286 },
1287 doElementsCollide: function (ele1, ele2, margin) {
1288 margin = margin || 0;
1289 ele1 = $(ele1);
1290 ele2 = $(ele2);
1291
1292 var dims1 = ele1.offset();
1293 dims1.right = dims1.left + ele1.width();
1294 dims1.bottom = dims1.top + ele1.height();
1295
1296 dims1.left -= margin;
1297 dims1.top -= margin;
1298 dims1.right += margin;
1299 dims1.bottom += margin;
1300
1301
1302 var dims2 = ele2.offset();
1303 dims2.right = dims2.left + ele2.width();
1304 dims2.bottom = dims2.top + ele2.height();
1305
1306 if (
1307 (
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)
1312 ) &&
1313 (
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))
1318 )
1319 {
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.
1325
1326 return true;
1327 }
1328
1329 return false;
1330 },
1331 scrollTo: function(x,y) {
1332 var headerOffset = this.getHeaderOffset();
1333 window.scrollTo(x,y-headerOffset);
1334 },
1335 getHeaderOffset: function() {
1336 if (typeof this.headerOffset === 'undefined') {
1337 this.headerOffset = 0;
1338 switch (modules['betteReddit'].options.pinHeader.value) {
1339 case 'none':
1340 break;
1341 case 'sub':
1342 this.theHeader = document.querySelector('#sr-header-area');
1343 break;
1344 case 'subanduser':
1345 this.theHeader = document.querySelector('#sr-header-area');
1346 break;
1347 case 'header':
1348 this.theHeader = document.querySelector('#header');
1349 break;
1350 }
1351 if (this.theHeader) {
1352 this.headerOffset = this.theHeader.offsetHeight + 6;
1353 }
1354 }
1355 return this.headerOffset;
1356 },
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;
1365 }
1366 }
1367 },
1368 stripHTML: function(str) {
1369 var regExp = /<\/?[^>]+>/gi;
1370 str = str.replace(regExp, "");
1371 return str;
1372 },
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;
1380 },
1381 autolink: redditCallbacks.autolink,
1382 raw_html_tag: redditCallbacks.raw_html_tag
1383 });
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'
1392 ];
1393 rendererConfig.html_attr_whitelist = [
1394 'href', 'title', 'src', 'alt', 'colspan',
1395 'rowspan', 'cellspacing', 'cellpadding', 'scope',
1396 'face', 'color', 'size', 'bgcolor', 'align'
1397 ];
1398 this.sanitizer = SnuOwnd.getParser({
1399 callbacks: callbacks,
1400 context: rendererConfig
1401 });
1402 }
1403 return this.sanitizer.render(htmlStr);
1404 },
1405 fadeElementOut: function(obj, speed, callback) {
1406 if (obj.getAttribute('isfading') === 'in') {
1407 return false;
1408 }
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();
1416 return true;
1417 } else {
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);
1422 }
1423 },
1424 fadeElementIn: function(obj, speed, finalOpacity) {
1425 finalOpacity = finalOpacity || 1;
1426 if (obj.getAttribute('isfading') === 'out') {
1427 return false;
1428 }
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';
1434 }
1435 if (obj.style.opacity >= finalOpacity) {
1436 obj.setAttribute('isfading',false);
1437 obj.style.opacity = finalOpacity;
1438 return true;
1439 } else {
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);
1444 }
1445 },
1446 setCursorPosition: function(form, pos) {
1447 elem = $(form)[0];
1448 if (!elem) return;
1449
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);
1457 range.select();
1458 }
1459
1460 return form;
1461 },
1462 setNewNotification: function() {
1463 $('#RESSettingsButton, #RESMainGearOverlay .gearIcon').addClass('newNotification').click(function() {
1464 location.href = '/r/RESAnnouncements';
1465 });
1466 },
1467 createMultiLock: function() {
1468 var locks = {};
1469 var count = 0;
1470
1471 return {
1472 lock: function(lockname, value) {
1473 if (typeof lockname === "undefined") return;
1474 if (locks[lockname]) return;
1475
1476 locks[lockname] = value || true;
1477 count++;
1478 return true;
1479 },
1480 unlock: function(lockname) {
1481 if (typeof lockname === "undefined") return;
1482 if (!locks[lockname]) return;
1483
1484 locks[lockname] = false;
1485 count--;
1486 return true;
1487 },
1488 locked: function(lockname) {
1489 if (typeof lockname !== "undefined") {
1490 // Is this lock set?
1491 return locks[lockname];
1492 } else {
1493 // Is any lock set?
1494 return count > 0;
1495 }
1496 }
1497 };
1498 },
1499 indexOptionTable: function(moduleID, optionKey, keyFieldIndex) {
1500 var source = modules[moduleID].options[optionKey].value;
1501 var keyIsList =
1502 modules[moduleID].options[optionKey].fields[keyFieldIndex].type === 'list' ?
1503 ',' :
1504 false;
1505 return RESUtils.indexArrayByProperty(source, keyFieldIndex, keyIsList);
1506 },
1507 indexArrayByProperty: function(source, keyIndex, keyValueSeparator) {
1508 if (!source || !source.length) {
1509 return function() { };
1510 }
1511
1512 var index = createIndex();
1513 return getItem;
1514
1515 function createIndex() {
1516 var index = {};
1517
1518 for (var i = 0, length = source.length; i < length; i++) {
1519 var item = source[i];
1520 var key = item && item[keyIndex];
1521 if (!key) continue;
1522
1523 if (keyValueSeparator) {
1524 var keys = key.toLowerCase().split(keyValueSeparator);
1525 for (var ki = 0, klength = keys.length; ki < klength; ki++) {
1526 key = keys[ki];
1527 index[key] = item;
1528 }
1529 } else {
1530 index[key] = item;
1531 }
1532 }
1533
1534 return index;
1535 }
1536
1537 function getItem(key) {
1538 key = key && key.toLowerCase();
1539 var item = index[key];
1540 return item;
1541 }
1542 },
1543 inList: function(needle, haystack, separator, isCaseSensitive) {
1544 if (!needle || !haystack) return false;
1545
1546 separator = separator || ',';
1547
1548 if (haystack.indexOf(separator) !== -1) {
1549 var haystacks = haystack.split(separator);
1550 if (RESUtils.inArray(needle, haystacks, isCaseSensitive)) {
1551 return true;
1552 }
1553 } else {
1554 if (caseSensitive) {
1555 return (needle == haystack);
1556 } else {
1557 return (needle.toLowerCase() == haystack.toLowerCase());
1558 }
1559 }
1560 },
1561 inArray: function(needle, haystacks, isCaseSensitive) {
1562 if (!isCaseSensitive) needle = needle.toLowerCase();
1563
1564 for (var i = 0, length = haystacks.length; i < length; i++) {
1565 if (isCaseSensitive) {
1566 if (needle == haystacks[i]) {
1567 return true;
1568 }
1569 } else {
1570 if (needle == haystacks[i].toLowerCase()) {
1571 return true;
1572 }
1573 }
1574 }
1575 },
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);
1581 }
1582 },
1583 // checkForUpdate: function(forceUpdate) {
1584 checkForUpdate: function() {
1585 if (RESUtils.currentSubreddit('RESAnnouncements')) {
1586 RESStorage.removeItem('RES.newAnnouncement','true');
1587 }
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();
1601 }
1602 RESStorage.setItem('RES.lastAnnouncementID', thisID);
1603 });
1604 /*
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
1611 var thisJSON = {
1612 requestType: 'compareVersion',
1613 url: jsonURL
1614 };
1615 chrome.extension.sendMessage(thisJSON, function(response) {
1616 // send message to background.html to open new tabs...
1617 outdated = RESUtils.compareVersion(response, forceUpdate);
1618 });
1619 } else if (BrowserDetect.isSafari()) {
1620 // we've got safari, so we need to hit up the background page to do cross domain XHR
1621 thisJSON = {
1622 requestType: 'compareVersion',
1623 url: jsonURL,
1624 forceUpdate: forceUpdate
1625 }
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
1629 thisJSON = {
1630 requestType: 'compareVersion',
1631 url: jsonURL,
1632 forceUpdate: forceUpdate
1633 }
1634 opera.extension.postMessage(JSON.stringify(thisJSON));
1635 } else {
1636 // we've got greasemonkey, so we can do cross domain XHR.
1637 GM_xmlhttpRequest({
1638 method: "GET",
1639 url: jsonURL,
1640 onload: function(response) {
1641 outdated = RESUtils.compareVersion(JSON.parse(response.responseText), forceUpdate);
1642 }
1643 });
1644 }
1645 */
1646 }
1647 },
1648 /*
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);
1654 if (forceUpdate) {
1655 $(RESConsole.RESCheckUpdateButton).html('You are out of date! <a target="_blank" href="http://reddit.honestbleeps.com/download">[click to update]</a>');
1656 }
1657 return true;
1658 } else {
1659 RESStorage.setItem('RESlatestVersion',response.latestVersion);
1660 RESStorage.setItem('RESoutdated','false');
1661 if (forceUpdate) {
1662 $(RESConsole.RESCheckUpdateButton).html('You are up to date!');
1663 }
1664 return false;
1665 }
1666 },
1667 */
1668 proEnabled: function() {
1669 return ((typeof modules['RESPro'] !== 'undefined') && (modules['RESPro'].isEnabled()));
1670 },
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-';
1681 }
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-';
1689 }
1690 switch(testCode) {
1691 case 8:
1692 niceString = "backspace"; // backspace
1693 break;
1694 case 9:
1695 niceString = "tab"; // tab
1696 break;
1697 case 13:
1698 niceString = "enter"; // enter
1699 break;
1700 case 16:
1701 niceString = "shift"; // shift
1702 break;
1703 case 17:
1704 niceString = "ctrl"; // ctrl
1705 break;
1706 case 18:
1707 niceString = "alt"; // alt
1708 break;
1709 case 19:
1710 niceString = "pause/break"; // pause/break
1711 break;
1712 case 20:
1713 niceString = "caps lock"; // caps lock
1714 break;
1715 case 27:
1716 niceString = "escape"; // escape
1717 break;
1718 case 33:
1719 niceString = "page up"; // page up, to avoid displaying alternate character and confusing people
1720 break;
1721 case 34:
1722 niceString = "page down"; // page down
1723 break;
1724 case 35:
1725 niceString = "end"; // end
1726 break;
1727 case 36:
1728 niceString = "home"; // home
1729 break;
1730 case 37:
1731 niceString = "left arrow"; // left arrow
1732 break;
1733 case 38:
1734 niceString = "up arrow"; // up arrow
1735 break;
1736 case 39:
1737 niceString = "right arrow"; // right arrow
1738 break;
1739 case 40:
1740 niceString = "down arrow"; // down arrow
1741 break;
1742 case 45:
1743 niceString = "insert"; // insert
1744 break;
1745 case 46:
1746 niceString = "delete"; // delete
1747 break;
1748 case 91:
1749 niceString = "left window"; // left window
1750 break;
1751 case 92:
1752 niceString = "right window"; // right window
1753 break;
1754 case 93:
1755 niceString = "select key"; // select key
1756 break;
1757 case 96:
1758 niceString = "numpad 0"; // numpad 0
1759 break;
1760 case 97:
1761 niceString = "numpad 1"; // numpad 1
1762 break;
1763 case 98:
1764 niceString = "numpad 2"; // numpad 2
1765 break;
1766 case 99:
1767 niceString = "numpad 3"; // numpad 3
1768 break;
1769 case 100:
1770 niceString = "numpad 4"; // numpad 4
1771 break;
1772 case 101:
1773 niceString = "numpad 5"; // numpad 5
1774 break;
1775 case 102:
1776 niceString = "numpad 6"; // numpad 6
1777 break;
1778 case 103:
1779 niceString = "numpad 7"; // numpad 7
1780 break;
1781 case 104:
1782 niceString = "numpad 8"; // numpad 8
1783 break;
1784 case 105:
1785 niceString = "numpad 9"; // numpad 9
1786 break;
1787 case 106:
1788 niceString = "multiply"; // multiply
1789 break;
1790 case 107:
1791 niceString = "add"; // add
1792 break;
1793 case 109:
1794 niceString = "subtract"; // subtract
1795 break;
1796 case 110:
1797 niceString = "decimal point"; // decimal point
1798 break;
1799 case 111:
1800 niceString = "divide"; // divide
1801 break;
1802 case 112:
1803 niceString = "F1"; // F1
1804 break;
1805 case 113:
1806 niceString = "F2"; // F2
1807 break;
1808 case 114:
1809 niceString = "F3"; // F3
1810 break;
1811 case 115:
1812 niceString = "F4"; // F4
1813 break;
1814 case 116:
1815 niceString = "F5"; // F5
1816 break;
1817 case 117:
1818 niceString = "F6"; // F6
1819 break;
1820 case 118:
1821 niceString = "F7"; // F7
1822 break;
1823 case 119:
1824 niceString = "F8"; // F8
1825 break;
1826 case 120:
1827 niceString = "F9"; // F9
1828 break;
1829 case 121:
1830 niceString = "F10"; // F10
1831 break;
1832 case 122:
1833 niceString = "F11"; // F11
1834 break;
1835 case 123:
1836 niceString = "F12"; // F12
1837 break;
1838 case 144:
1839 niceString = "num lock"; // num lock
1840 break;
1841 case 145:
1842 niceString = "scroll lock"; // scroll lock
1843 break;
1844 case 186:
1845 niceString = ";"; // semi-colon
1846 break;
1847 case 187:
1848 niceString = "="; // equal-sign
1849 break;
1850 case 188:
1851 niceString = ","; // comma
1852 break;
1853 case 189:
1854 niceString = "-"; // dash
1855 break;
1856 case 190:
1857 niceString = "."; // period
1858 break;
1859 case 191:
1860 niceString = "/"; // forward slash
1861 break;
1862 case 192:
1863 niceString = "`"; // grave accent
1864 break;
1865 case 219:
1866 niceString = "["; // open bracket
1867 break;
1868 case 220:
1869 niceString = "\\"; // back slash
1870 break;
1871 case 221:
1872 niceString = "]"; // close bracket
1873 break;
1874 case 222:
1875 niceString = "'"; // single quote
1876 break;
1877 default:
1878 niceString = String.fromCharCode(testCode);
1879 break;
1880 }
1881 return keyComboString + niceString;
1882 },
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;
1891 if (usformat) {
1892 fullString = month+'-'+day+'-'+year;
1893 }
1894 return fullString;
1895 },
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;
1906 return fullString;
1907 },
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
1911 if (!newdate) {
1912 newdate = new Date();
1913 }
1914
1915 var amonth = origdate.getUTCMonth() + 1;
1916 var aday = origdate.getUTCDate();
1917 var ayear = origdate.getUTCFullYear();
1918
1919 var tyear = newdate.getUTCFullYear();
1920 var tmonth = newdate.getUTCMonth() + 1;
1921 var tday = newdate.getUTCDate();
1922
1923 var y = 1;
1924 var mm = 1;
1925 var d = 1;
1926 var a2 = 0;
1927 var a1 = 0;
1928 var f = 28;
1929
1930 if (((tyear % 4 === 0) && (tyear % 100 !== 0)) || (tyear % 400 === 0)) {
1931 f = 29;
1932 }
1933
1934 var m = [31, f, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
1935
1936 var dyear = tyear - ayear;
1937
1938 var dmonth = tmonth - amonth;
1939 if (dmonth < 0 && dyear > 0) {
1940 dmonth = dmonth + 12;
1941 dyear--;
1942 }
1943
1944 var dday = tday - aday;
1945 if (dday < 0) {
1946 if (dmonth > 0) {
1947 var ma = amonth + tmonth;
1948
1949 if (ma >= 12) { ma = ma - 12; }
1950 if (ma < 0) { ma = ma + 12; }
1951 dday = dday + m[ma];
1952 dmonth--;
1953 if (dmonth < 0) {
1954 dyear--;
1955 dmonth = dmonth + 12;
1956 }
1957 } else {
1958 dday = 0;
1959 }
1960 }
1961
1962 var returnString = '';
1963
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; }
1970 if (y === 1){
1971 if (dyear === 1) {
1972 returnString += dyear + " year";
1973 } else {
1974 returnString += dyear + " years";
1975 }
1976 }
1977 if ((a1 === 1) && (a2 === 0)) { returnString += " and "; }
1978 if ((a1 === 1) && (a2 === 1)) { returnString += ", "; }
1979 if (mm === 1){
1980 if (dmonth === 1) {
1981 returnString += dmonth + " month";
1982 } else {
1983 returnString += dmonth + " months";
1984 }
1985 }
1986 if (a2 === 1) { returnString += " and "; }
1987 if (d === 1) {
1988 if (dday === 1) {
1989 returnString += dday + " day";
1990 } else {
1991 returnString += dday + " days";
1992 }
1993 }
1994 if (returnString === '') {
1995 returnString = '0 days';
1996 }
1997 return returnString;
1998 },
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();
2010 }, false);
2011 }
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> \
2028 </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> \
2031 <ol> \
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> \
2035 </ol> \
2036 <br> \
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> \
2040 </div> \
2041 <div id=\"RESFeatureRequest\"> \
2042 So you want to request a feature, great! Please just consider the following, first:<br> \
2043 <ol> \
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> \
2046 </ol> \
2047 <br> \
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> \
2051 </div> \
2052 </div>";
2053 $(this.submittingToEnhancement).html(submittingHTML);
2054 insertAfter(textDesc, this.submittingToEnhancement);
2055 setTimeout(function() {
2056 $('#RESSubmitBug').click(
2057 function() {
2058 $('#RESSubmitOptions').fadeOut(
2059 function() {
2060 $('#RESBugReport').fadeIn();
2061 GM_xmlhttpRequest({
2062 method: "GET",
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>');
2069 });
2070 }
2071 });
2072 }
2073 );
2074 }
2075 );
2076 $('#RESSubmitFeatureRequest').click(
2077 function() {
2078 $('#RESSubmitOptions').fadeOut(
2079 function() {
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>');
2085 });
2086 });
2087 }
2088 );
2089 }
2090 );
2091 $('#submittingBug').click(
2092 function() {
2093 $('#sr-autocomplete').val('RESIssues');
2094 $('li a.text-button').click();
2095 $('#submittingToEnhancement').fadeOut();
2096
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.';
2106 }
2107 );
2108 $('#submittingFeature').click(
2109 function() {
2110 $('#sr-autocomplete').val('Enhancement');
2111 $('#submittingToEnhancement').fadeOut();
2112 title.value = '[feature request] Please summarize your feature request here, and elaborate in the selftext.';
2113 }
2114 );
2115 $('#RESSubmitOther').click(
2116 function() {
2117 $('#sr-autocomplete').val('Enhancement');
2118 $('#submittingToEnhancement').fadeOut();
2119 title.value = '';
2120 }
2121 );
2122 $('#submittingToEnhancement').fadeIn();
2123 }, 1000);
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...') {
2127 title.value = '';
2128 }
2129 }
2130 }
2131 }
2132 },
2133 isEmpty: function(obj) {
2134 for(var prop in obj) {
2135 if(obj.hasOwnProperty(prop))
2136 return false;
2137 }
2138 return true;
2139 },
2140 deleteCookie: function(cookieName) {
2141 var requestJSON = {
2142 requestType: 'deleteCookie',
2143 cname: cookieName
2144 };
2145
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);
2154 }
2155 },
2156 openLinkInNewTab: function(url, focus) {
2157 var thisJSON;
2158 if (BrowserDetect.isChrome()) {
2159 thisJSON = {
2160 requestType: 'openLinkInNewTab',
2161 linkURL: url,
2162 button: focus
2163 };
2164 // send message to background.html to open new tabs...
2165 chrome.extension.sendMessage(thisJSON);
2166 } else if (BrowserDetect.isSafari()) {
2167 thisJSON = {
2168 requestType: 'openLinkInNewTab',
2169 linkURL: url,
2170 button: focus
2171 };
2172 safari.self.tab.dispatchMessage("openLinkInNewTab", thisJSON);
2173 } else if (BrowserDetect.isOpera()) {
2174 thisJSON = {
2175 requestType: 'openLinkInNewTab',
2176 linkURL: url,
2177 button: focus
2178 };
2179 opera.extension.postMessage(JSON.stringify(thisJSON));
2180 } else if (BrowserDetect.isFirefox()) {
2181 thisJSON = {
2182 requestType: 'openLinkInNewTab',
2183 linkURL: url,
2184 button: focus
2185 };
2186 self.postMessage(thisJSON);
2187 } else {
2188 window.open(url);
2189 }
2190 },
2191 notification: function(contentObj, delay) {
2192 var content;
2193 if (typeof contentObj.message === 'undefined') {
2194 if (typeof contentObj === 'string') {
2195 content = contentObj;
2196 } else {
2197 return false;
2198 }
2199 } else {
2200 content = contentObj.message;
2201 }
2202
2203 var header;
2204 if (contentObj.header) {
2205 header = contentObj.header;
2206 } else {
2207 header = [];
2208
2209 if (contentObj.moduleID && modules[contentObj.moduleID]) {
2210 header.push(modules[contentObj.moduleID].moduleName);
2211 }
2212
2213 if (contentObj.type === 'error') {
2214 header.push('Error');
2215 } else {
2216 header.push('Notification');
2217 }
2218
2219
2220 header = header.join(' ');
2221 }
2222
2223 if (contentObj.moduleID && modules[contentObj.moduleID]) {
2224 header += modules['settingsNavigation'].makeUrlHashLink(contentObj.moduleID, contentObj.optionKey, ' ', 'gearIcon');
2225 }
2226
2227
2228 if (typeof this.notificationCount === 'undefined') {
2229 this.adFrame = document.body.querySelector('#ad-frame');
2230 if (this.adFrame) {
2231 this.adFrame.style.display = 'none';
2232 }
2233 this.notificationCount = 0;
2234 this.notificationTimers = [];
2235 this.RESNotifications = createElementWithID('div','RESNotifications');
2236 document.body.appendChild(this.RESNotifications);
2237 }
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">&times;</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);
2246 }, false);
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++;
2253 },
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);
2262 }, delay);
2263 RESUtils.notificationTimers[thisNotificationID] = thisTimer;
2264 thisNotification.addEventListener('mouseover',RESUtils.cancelCloseNotificationTimer, false);
2265 thisNotification.removeEventListener('mouseout',RESUtils.setCloseNotification,false);
2266 },
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);
2273 },
2274 closeNotification: function(ele) {
2275 RESUtils.fadeElementOut(ele, 0.1, RESUtils.notificationClosed);
2276 },
2277 notificationClosed: function(ele) {
2278 var notifications = RESUtils.RESNotifications.querySelectorAll('.RESNotification');
2279 var destroyed = 0;
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]);
2283 destroyed++;
2284 }
2285 }
2286 if (destroyed == notifications.length) {
2287 RESUtils.RESNotifications.style.display = 'none';
2288 if (RESUtils.adFrame) RESUtils.adFrame.style.display = 'block';
2289 }
2290
2291 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'notification');
2292 },
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');
2301 var tableAttr = '';
2302 if (isTable) {
2303 tableAttr = ' tableOption="true"';
2304 }
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;
2310 if (enabled) {
2311 this.classList.remove('enabled');
2312 } else {
2313 this.classList.add('enabled');
2314 }
2315 }, false);
2316 if (enabled) thisToggle.classList.add('enabled');
2317 return thisToggle;
2318 },
2319 addCommas: function(nStr) {
2320 nStr += '';
2321 var x = nStr.split('.');
2322 var x1 = x[0];
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');
2327 }
2328 return x1 + x2;
2329 },
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 ];
2334
2335 var description = [];
2336 description.push('<table>');
2337
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);
2344 }
2345 }
2346 description.push('</table>');
2347 description = description.join('\n');
2348
2349 return description;
2350 },
2351 xhrCache: function(operation) {
2352 var thisJSON = {
2353 requestType: 'XHRCache',
2354 operation: operation
2355 };
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);
2364 }
2365 },
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];
2374 }
2375
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);
2383 });
2384 RESUtils.watchers.siteTable.forEach(function(callback) {
2385 if (callback) callback(mutation.addedNodes[0]);
2386 });
2387 }
2388 });
2389 });
2390
2391 observer.observe(siteTable, {
2392 attributes: false,
2393 childList: true,
2394 characterData: false
2395 });
2396 } else {
2397 // Opera doesn't support MutationObserver - so we need this for Opera support.
2398 if (siteTable) {
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);
2403 });
2404 }
2405 }, true);
2406 }
2407 }
2408 } else {
2409 // initialize sitetable observer...
2410 siteTable = document.querySelector('.commentarea > .sitetable');
2411
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);
2422 });
2423 RESUtils.watchers.newComments.forEach(function(callback) {
2424 if (callback) callback(newCommentEntry);
2425 });
2426 }
2427 }
2428 });
2429 });
2430
2431 observer.observe(siteTable, {
2432 attributes: false,
2433 childList: true,
2434 characterData: false
2435 });
2436 } else {
2437 // Opera doesn't support MutationObserver - so we need this for Opera support.
2438 if (siteTable) {
2439 siteTable.addEventListener('DOMNodeInserted', RESUtils.mutationEventCommentHandler, false);
2440 }
2441 }
2442 }
2443
2444 $('.entry div.expando').each(function() {
2445 RESUtils.addSelfTextObserver(this);
2446 });
2447
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);
2451
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);
2457 });
2458
2459 },
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);
2477 });
2478 }
2479 }
2480 },
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);
2487 }
2488 },
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);
2504 });
2505
2506 // check for "load new comments" links within this group as well...
2507 $(nodeList[j]).find('.morecomments a').click(RESUtils.addNewCommentObserverToTarget);
2508
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);
2517 });
2518 }
2519 }
2520 }
2521 }
2522 }
2523
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);
2528 // });
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);
2532 // });
2533
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();
2540 }
2541 });
2542
2543 observer.observe(mutationNodeToObserve, {
2544 attributes: false,
2545 childList: true,
2546 characterData: false
2547 });
2548 } else {
2549 mutationNodeToObserve.addEventListener('DOMNodeInserted', RESUtils.mutationEventCommentHandler, false);
2550 }
2551 },
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) {
2560 callback(form[0]);
2561 });
2562 } else {
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]);
2568 });
2569 }
2570 }
2571 });
2572
2573 observer.observe(commentsFormParent, {
2574 attributes: false,
2575 childList: true,
2576 characterData: false
2577 });
2578 } else {
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);
2585 });
2586 } else {
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]);
2592 });
2593 }
2594 }
2595 }, true);
2596 }
2597 },
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) {
2606 callback(form[0]);
2607 });
2608 }
2609 });
2610
2611 observer.observe(selfTextParent, {
2612 attributes: false,
2613 childList: true,
2614 characterData: false
2615 });
2616 } else {
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);
2623 });
2624 }
2625 }, true);
2626 }
2627 },
2628 watchForElement: function(type, callback) {
2629 switch(type) {
2630 case 'siteTable':
2631 RESUtils.watchers.siteTable.push(callback);
2632 break;
2633 case 'newComments':
2634 RESUtils.watchers.newComments.push(callback);
2635 break;
2636 case 'selfText':
2637 RESUtils.watchers.selfText.push(callback);
2638 break;
2639 case 'newCommentsForms':
2640 RESUtils.watchers.newCommentsForms.push(callback);
2641 break;
2642 }
2643 },
2644 watchers: {
2645 siteTable: [],
2646 newComments: [],
2647 selfText: [],
2648 newCommentsForms: []
2649 },
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"
2653 //
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;
2660
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");
2664
2665 return !content && this.COMMENT_CODE_REGEX.test(href);
2666 },
2667 /*
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)
2674 */
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];
2681 }
2682 if (time !== null && call !== null) {
2683 RESUtils.debounceTimeouts[name] = window.setTimeout(function() {
2684 delete RESUtils.debounceTimeouts[name];
2685 call(data);
2686 }, time);
2687 }
2688 },
2689 toolTipTimers: {},
2690 /*
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.
2694 */
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;
2700 var counter = 0;
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;
2706 }
2707 if (counter < array.length) {
2708 window.setTimeout(doChunk, delay);
2709 }
2710 }
2711 window.setTimeout(doChunk, delay);
2712 },
2713 getComputedStyle: function(elem, property){
2714 if (elem.constructor === String) {
2715 elem = document.querySelector(elem);
2716 } else if (!(elem instanceof Node)) {
2717 return undefined;
2718 }
2719 var strValue;
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();
2725 });
2726 strValue = oElm.currentStyle[property];
2727 }
2728 return strValue;
2729 },
2730 hover: {
2731 defaults: {
2732 openDelay: 500,
2733 fadeDelay: 500,
2734 fadeSpeed: 0.3,
2735 width: 512,
2736 closeOnMouseOut: true
2737 },
2738 container: null,
2739 /*
2740 The contents of state are as follows:
2741 state: {
2742 //The DOM element that triggered the hover popup.
2743 element: null,
2744 //Resolved values for timing, etc.
2745 options: null,
2746 //Usecase specific object
2747 context: null,
2748 callback: null,
2749 }*/
2750 state: null,
2751 showTimer: null,
2752 hideTimer: null,
2753 begin: function(onElement, conf, callback, context) {
2754 var hover = RESUtils.hover;
2755 if (hover.container === null) hover.create();
2756 if (hover.state !== null) {
2757 hover.close(false);
2758 }
2759 var state = hover.state = {
2760 element: onElement,
2761 options: $.extend({}, hover.defaults, conf),
2762 context: context,
2763 callback: callback,
2764 };
2765 hover.showTimer = setTimeout(function() {
2766 hover.cancelShowTimer();
2767 hover.clearShowListeners();
2768 hover.open();
2769
2770 hover.state.element.addEventListener('mouseout', hover.startHideTimer, false);
2771 }, state.options.openDelay);
2772
2773 state.element.addEventListener('click', hover.cancelShow, false);
2774 state.element.addEventListener('mouseout', hover.cancelShow, false);
2775 },
2776
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"> \
2782 </div>').get(0);
2783
2784 document.body.appendChild(container);
2785
2786 $(container).hover(function() {
2787 if (RESUtils.hover.state !== null) {
2788 RESUtils.hover.cancelHideTimer();
2789 }
2790 }, function() {
2791 if (RESUtils.hover.state !== null) {
2792 RESUtils.hover.cancelHideTimer();
2793 if (RESUtils.hover.state.options.closeOnMouseOut === true) {
2794 RESUtils.hover.startHideTimer();
2795 }
2796 }
2797 });
2798
2799 $(container).on('click', '.RESCloseButton', function() {
2800 RESUtils.hover.close(true);
2801 });
2802 RESUtils.hover.container = container;
2803
2804 var css = '';
2805
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; }';
2811
2812 RESUtils.addCSS(css);
2813 },
2814 open: function() {
2815 var hover = RESUtils.hover;
2816 var def = $.Deferred();
2817 def.promise()
2818 .progress(hover.set)
2819 .done(hover.set)
2820 .fail(hover.close);
2821 hover.state.callback(def, hover.state.element, hover.state.context);
2822 },
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);
2828
2829 var XY=RESUtils.getXYpos(hover.state.element);
2830
2831 var width = $(hover.state.element).width();
2832 var tooltipWidth = $(container).width();
2833 tooltipWidth = hover.state.options.width;
2834
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');
2839 $(container).css({
2840 top: XY.y - 14,
2841 left: XY.x - tooltipWidth - 30,
2842 width: tooltipWidth
2843 });
2844 } else {
2845 container.classList.remove('right');
2846 $(container).css({
2847 top: XY.y - 14,
2848 left: XY.x + width + 25,
2849 width: tooltipWidth
2850 });
2851 }
2852 },
2853 cancelShow: function(e) {
2854 RESUtils.hover.close(true);
2855 },
2856 clearShowListeners: function() {
2857 if (RESUtils.hover.state === null) return;
2858 var element = RESUtils.hover.state.element;
2859 var func = RESUtils.hover.cancelShow;
2860
2861 element.removeEventListener('click', func, false);
2862 element.removeEventListener('mouseout', func, false);
2863 },
2864 cancelShowTimer: function() {
2865 if (RESUtils.hover.showTimer === null) return;
2866 clearTimeout(RESUtils.hover.showTimer);
2867 RESUtils.hover.showTimer = null;
2868 },
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);
2875 }
2876 },
2877 cancelHideTimer: function() {
2878 var hover = RESUtils.hover;
2879 if (RESUtils.hover.state !== null) {
2880 hover.state.element.removeEventListener('mouseout', hover.startHideTimer, false);
2881 }
2882 if (hover.hideTimer === null) return;
2883 clearTimeout(hover.hideTimer);
2884 hover.hideTimer = null;
2885 },
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();
2893 hover.state = null;
2894 }
2895 if (fade && hover.state !== null) {
2896 RESUtils.fadeElementOut(hover.container, hover.state.options.fadeSpeed, afterHide);
2897 } else {
2898 $(hover.container).hide(afterHide);
2899 }
2900 }
2901 }
2902 };
2903 // end RESUtils;
2904
2905 // Create a nice alert function...
2906 var gdAlert = {
2907 container: false,
2908 overlay: "",
2909
2910 init: function(callback) {
2911 //init
2912 var alertCSS = '#alert_message { ' +
2913 'display: none;' +
2914 'opacity: 0.0;' +
2915 'background-color: #EFEFEF;' +
2916 'border: 1px solid black;' +
2917 'color: black;' +
2918 'font-size: 10px;' +
2919 'padding: 20px;' +
2920 'padding-left: 60px;' +
2921 'padding-right: 60px;' +
2922 'position: fixed!important;' +
2923 'position: absolute;' +
2924 'width: 400px;' +
2925 'float: left;' +
2926 'z-index: 1000000201;' +
2927 'text-align: left;' +
2928 'left: auto;' +
2929 'top: auto;' +
2930 '}' +
2931 '#alert_message .button {' +
2932 'border: 1px solid black;' +
2933 'font-weight: bold;' +
2934 'font-size: 10px;' +
2935 'padding: 4px;' +
2936 'padding-left: 7px;' +
2937 'padding-right: 7px;' +
2938 'float: left;' +
2939 'background-color: #DFDFDF;' +
2940 'cursor: pointer;' +
2941 '}' +
2942 '#alert_message span {' +
2943 'display: block;' +
2944 'margin-bottom: 15px; ' +
2945 '}'+
2946 '#alert_message_background {' +
2947 'position: fixed; top: 0; left: 0; bottom: 0; right: 0;' +
2948 'background-color: #333333; z-index: 100000200;' +
2949 '}';
2950
2951 GM_addStyle(alertCSS);
2952
2953 gdAlert.populateContainer(callback);
2954
2955 },
2956
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);
2972 } else {
2973 /* if (this.okButton) {
2974 gdAlert.container.removeChild(this.okButton);
2975 delete this.okButton;
2976 } */
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);
2982 }
2983 var br = document.createElement('br');
2984 br.setAttribute('style','clear: both');
2985 gdAlert.container.appendChild(br);
2986 document.body.appendChild(gdAlert.container);
2987 },
2988 open: function(text, callback) {
2989 if (gdAlert.isOpen) {
2990 return;
2991 }
2992 gdAlert.isOpen = true;
2993 gdAlert.populateContainer(callback);
2994
2995 //set message
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();
3000
3001 //create site overlay
3002 gdAlert.overlay = createElementWithID("div", "alert_message_background");
3003 document.body.appendChild(gdAlert.overlay);
3004
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];
3012
3013 gdAlert.container.style.top = ((winH / 2) - 90) + "px";
3014 gdAlert.container.style.left = ((gdAlert.getPageSize()[0] / 2) - 155) + "px";
3015
3016 /*
3017 new Effect.Appear(gdAlert.container, {duration: 0.2});
3018 new Effect.Opacity(gdAlert.overlay, {duration: 0.2, to: 0.8});
3019 */
3020 RESUtils.fadeElementIn(gdAlert.container, 0.3);
3021 RESUtils.fadeElementIn(gdAlert.overlay, 0.3);
3022 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'gdAlert');
3023 },
3024
3025 close: function() {
3026 gdAlert.isOpen = false;
3027 /*
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);
3031 }});
3032 */
3033 RESUtils.fadeElementOut(gdAlert.container, 0.3);
3034 RESUtils.fadeElementOut(gdAlert.overlay, 0.3);
3035 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'gdAlert');
3036 },
3037
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;
3049 }
3050
3051 var windowWidth, windowHeight;
3052
3053 if (self.innerHeight) { // all except Explorer
3054 if(document.documentElement.clientWidth){
3055 windowWidth = document.documentElement.clientWidth;
3056 } else {
3057 windowWidth = self.innerWidth;
3058 }
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;
3066 }
3067
3068 // for small pages with total height less then height of the viewport
3069 if(yScroll < windowHeight){
3070 pageHeight = windowHeight;
3071 } else {
3072 pageHeight = yScroll;
3073 }
3074
3075 // for small pages with total width less then width of the viewport
3076 if(xScroll < windowWidth){
3077 pageWidth = xScroll;
3078 } else {
3079 pageWidth = windowWidth;
3080 }
3081 return [pageWidth,pageHeight];
3082 }
3083 };
3084
3085 //overwrite the alert function
3086 var alert = function(text, callback) {
3087 if (gdAlert.container === false) {
3088 gdAlert.init(callback);
3089 }
3090 gdAlert.open(text, callback);
3091 };
3092
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]);
3098 }
3099 localStorage.setItem('copyComplete','true');
3100 localStorage.removeItem('RES.lsTest');
3101 RESUtils.notification('Data transfer complete. You may now uninstall the Greasemonkey script');
3102 }
3103
3104 // jquery plugin CSS
3105 RESUtils.addCSS(tokenizeCSS);
3106 RESUtils.addCSS(guidersCSS);
3107
3108 // define the RESConsole class
3109 var RESConsole = {
3110 modalOverlay: '',
3111 RESConsoleContainer: '',
3112 RESMenuItems: [],
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];
3125 }
3126 },
3127 addConsoleDropdown: function() {
3128 this.gearOverlay = createElementWithID('div','RESMainGearOverlay');
3129 this.gearOverlay.setAttribute('class','RESGearOverlay');
3130 $(this.gearOverlay).html('<div class="gearIcon"></div>');
3131
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();
3138 RESConsole.open();
3139 }, true);
3140 var thisDonateButton = this.prefsDropdown.querySelector('#RES-donate');
3141 thisDonateButton.addEventListener('click', function() {
3142 RESUtils.openLinkInNewTab('http://redditenhancementsuite.com/contribute.html', true);
3143 }, true);
3144 $(this.prefsDropdown).mouseleave(function() {
3145 RESConsole.hidePrefsDropdown();
3146 });
3147 $(this.prefsDropdown).mouseenter(function() {
3148 clearTimeout(RESConsole.prefsTimer);
3149 });
3150 $(this.gearOverlay).mouseleave(function() {
3151 RESConsole.prefsTimer = setTimeout(function() {
3152 RESConsole.hidePrefsDropdown();
3153 }, 1000);
3154 });
3155 document.body.appendChild(this.gearOverlay);
3156 document.body.appendChild(this.prefsDropdown);
3157 if (RESStorage.getItem('RES.newAnnouncement','true')) {
3158 RESUtils.setNewNotification();
3159 }
3160 },
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');
3175 },
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');
3181 },
3182 resetModulePrefs: function() {
3183 prefs = {
3184 'userTagger': true,
3185 'betteReddit': true,
3186 'singleClick': true,
3187 'subRedditTagger': true,
3188 'uppersAndDowners': true,
3189 'keyboardNav': true,
3190 'commentPreview': true,
3191 'showImages': true,
3192 'showKarma': true,
3193 'usernameHider': false,
3194 'accountSwitcher': true,
3195 'styleTweaks': true,
3196 'filteReddit': true,
3197 'spamButton': false,
3198 'bitcointip': false,
3199 'RESPro': false
3200 };
3201 this.setModulePrefs(prefs);
3202 return prefs;
3203 },
3204 getAllModulePrefs: function(force) {
3205 var storedPrefs;
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);
3216 } else {
3217 // looks like this is the first time RES has been run - set prefs to defaults...
3218 storedPrefs = this.resetModulePrefs();
3219 }
3220 if (storedPrefs === null) {
3221 storedPrefs = {};
3222 }
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.
3224 var prefs = {};
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;
3233 } else {
3234 prefs[module] = false;
3235 }
3236 }
3237 if ((typeof prefs !== 'undefined') && (prefs !== 'undefined') && (prefs)) {
3238 this.getAllModulePrefsCached = prefs;
3239 return prefs;
3240 }
3241 },
3242 getModulePrefs: function(moduleID) {
3243 if (moduleID) {
3244 var prefs = this.getAllModulePrefs();
3245 return prefs[moduleID];
3246 } else {
3247 alert('no module name specified for getModulePrefs');
3248 }
3249 },
3250 setModulePrefs: function(prefs) {
3251 if (prefs !== null) {
3252 RESStorage.setItem('RES.modulePrefs', JSON.stringify(prefs));
3253 return prefs;
3254 } else {
3255 alert('error - no prefs specified');
3256 }
3257 },
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) {
3266 e.preventDefault();
3267 return false;
3268 }, true);
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);
3286
3287 // Create the search bar and place it in the top bar
3288 var RESSearchContainer = modules['settingsNavigation'].renderSearchForm();
3289 RESConsoleTopBar.appendChild(RESSearchContainer);
3290
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) {
3300 e.preventDefault();
3301 RESConsole.close();
3302 }, true);
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);
3308 }
3309 }
3310 this.categories.sort();
3311 // create the menu
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) {
3320 e.preventDefault();
3321 RESConsole.menuClick(this);
3322 }, true);
3323 RESMenu.appendChild(thisMenuItem);
3324 }
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);
3334
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...
3338 e.preventDefault();
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;
3344 }
3345 });
3346
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({
3352 display: "block",
3353 top: RESUtils.mouseY + "px",
3354 left: RESUtils.mouseX + "px;"
3355 });
3356 // RESConsole.keyCodeModal.style.display = 'block';
3357 RESConsole.captureKey = true;
3358 RESConsole.captureKeyID = this.getAttribute('capturefor');
3359 },
3360 blur: function(e) {
3361 $(RESConsole.keyCodeModal).css("display", "none");
3362 }
3363 });
3364
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);
3368 },
3369 drawConfigPanel: function(category) {
3370 if (!category) return;
3371
3372 this.drawConfigPanelCategory(category);
3373 },
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);
3378 }
3379 moduleList.sort(function(a,b) {
3380 if (modules[a].moduleName.toLowerCase() > modules[b].moduleName.toLowerCase()) return 1;
3381 return -1;
3382 });
3383
3384 return moduleList;
3385 },
3386 drawConfigPanelCategory: function(category, moduleList) {
3387 $(this.RESConsoleConfigPanel).empty();
3388
3389 /*
3390 var moduleTest = RESStorage.getItem('moduleTest');
3391 if (moduleTest) {
3392 console.log(moduleTest);
3393 // TEST loading stored modules...
3394 var evalTest = eval(moduleTest);
3395 }
3396 */
3397 moduleList = moduleList || this.getModuleIDsByCategory(category);
3398
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');
3407 }
3408 thisModuleButton.setAttribute('moduleID', modules[thisModule].moduleID);
3409 thisModuleButton.addEventListener('click', function(e) {
3410 RESConsole.showConfigOptions(this.getAttribute('moduleID'));
3411 }, false);
3412 this.RESConfigPanelModulesPane.appendChild(thisModuleButton);
3413 }
3414 this.RESConsoleConfigPanel.appendChild(this.RESConfigPanelModulesPane);
3415
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);
3420 },
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');
3426 },
3427 drawOptionInput: function(moduleID, optionName, optionObject, isTable) {
3428 var thisOptionFormEle;
3429 switch(optionObject.type) {
3430 case 'textarea':
3431 // textarea...
3432 thisOptionFormEle = createElementWithID('textarea', optionName);
3433 thisOptionFormEle.setAttribute('type','textarea');
3434 thisOptionFormEle.setAttribute('moduleID',moduleID);
3435 $(thisOptionFormEle).html(escapeHTML(optionObject.value));
3436 break;
3437 case 'text':
3438 // text...
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);
3444 break;
3445 case 'button':
3446 // button...
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);
3452 break;
3453 case 'list':
3454 // list...
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 = '';
3462 var prepop = [];
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]});
3466 }
3467 setTimeout(function() {
3468 $(thisOptionFormEle).tokenInput(optionObject.source, {
3469 method: "POST",
3470 queryParam: "query",
3471 theme: "facebook",
3472 allowFreeTagging: true,
3473 zindex: 999999999,
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
3478 });
3479 }, 100);
3480 break;
3481 case 'password':
3482 // password...
3483 thisOptionFormEle = createElementWithID('input', optionName);
3484 thisOptionFormEle.setAttribute('type','password');
3485 thisOptionFormEle.setAttribute('moduleID',moduleID);
3486 thisOptionFormEle.setAttribute('value',optionObject.value);
3487 break;
3488 case 'boolean':
3489 // checkbox
3490 /*
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);
3497 }
3498 */
3499 thisOptionFormEle = RESUtils.toggleButton(optionName, optionObject.value, null, null, isTable);
3500 break;
3501 case 'enum':
3502 // radio buttons
3503 if (typeof optionObject.values === 'undefined') {
3504 alert('misconfigured enum option in module: ' + moduleID);
3505 } else {
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');
3521 }
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);
3527 }
3528 }
3529 break;
3530 case 'keycode':
3531 // keycode - shows a key value, but stores a keycode and possibly shift/alt/ctrl combo.
3532 var realOptionFormEle = $("<input>").attr({
3533 id: optionName,
3534 type: "text",
3535 class: "keycode",
3536 moduleID: moduleID
3537 }).css({
3538 border: "1px solid red",
3539 display: "none"
3540 }).val(optionObject.value);
3541 if (isTable) realOptionFormEle.attr('tableOption','true');
3542
3543 var thisKeyCodeDisplay = $("<input>").attr({
3544 id: optionName+"-display",
3545 type: "text",
3546 capturefor: optionName,
3547 displayonly: "true"
3548 }).val(RESUtils.niceKeyCode(optionObject.value));
3549 thisOptionFormEle = $("<div>").append(realOptionFormEle).append(thisKeyCodeDisplay)[0];
3550 break;
3551 default:
3552 console.log('misconfigured option in module: ' + moduleID);
3553 break;
3554 }
3555 if (isTable) {
3556 thisOptionFormEle.setAttribute('tableOption','true');
3557 }
3558 return thisOptionFormEle;
3559 },
3560 enableModule: function(moduleID, onOrOff) {
3561 var prefs = this.getAllModulePrefs(true);
3562 prefs[moduleID] = !!onOrOff;
3563 this.setModulePrefs(prefs);
3564 },
3565 showConfigOptions: function(moduleID) {
3566 if (!modules[moduleID]) return;
3567 RESConsole.drawConfigOptions(moduleID);
3568 RESConsole.updateSelectedModule(moduleID);
3569 RESConsole.currentModule = moduleID;
3570
3571 RESConsole.RESConsoleContent.scrollTop = 0;
3572
3573 modules['settingsNavigation'].setUrlHash(moduleID);
3574 },
3575 drawConfigOptions: function(moduleID) {
3576 if (modules[moduleID] && modules[moduleID].hidden) return;
3577 var thisOptions = RESUtils.getOptions(moduleID);
3578 var optCount = 0;
3579
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');
3595 if (enabled) {
3596 activePane.classList.remove('enabled');
3597 this.classList.remove('enabled');
3598 RESConsole.moduleOptionsScrim.classList.add('visible');
3599 $('#moduleOptionsSave').hide();
3600 } else {
3601 activePane.classList.add('enabled');
3602 this.classList.add('enabled');
3603 RESConsole.moduleOptionsScrim.classList.remove('visible');
3604 $('#moduleOptionsSave').fadeIn();
3605 }
3606 RESConsole.enableModule(this.getAttribute('moduleID'), !enabled);
3607 }, true);
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);
3616 }, true);
3617 this.RESConsoleConfigPanel.appendChild(thisSaveButton);
3618 var thisSaveStatus = createElementWithID('div','moduleOptionsSaveStatus','saveStatus');
3619 thisHeader.appendChild(thisSaveStatus);
3620 break;
3621 }
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)) {
3632 optCount++;
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');
3645 } else {
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);
3661 }
3662 // add delete column
3663 thisTH = document.createElement('th');
3664 $(thisTH).text('delete');
3665 thisTableHeader.appendChild(thisTH);
3666 // add move column
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);
3691 }
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);
3699 // add move handle
3700 thisTD = document.createElement('td');
3701 var thisHandle = document.createElement('div');
3702 $(thisHandle)
3703 .html("&#x22ee;&#x22ee;")
3704 .addClass('handle')
3705 .appendTo(thisTD);
3706 thisTR.appendChild(thisTD);
3707 thisTbody.appendChild(thisTR);
3708 }
3709 }
3710 thisTable.appendChild(thisTbody);
3711 var thisOptionFormEle = thisTable;
3712 }
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');
3741 if (firstText) {
3742 setTimeout(function() {
3743 firstText.focus();
3744 }, 200);
3745 }
3746 }
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);
3754 // add move handle
3755 thisTD = document.createElement('td');
3756 var thisHandle = document.createElement('div');
3757 $(thisHandle)
3758 .html("&#x22ee;&#x22ee;")
3759 .addClass('handle')
3760 .appendTo(newRow);
3761
3762 var thisLen = (modules[moduleID].options[optionName].value) ? modules[moduleID].options[optionName].value.length : 0;
3763 $(thisTR).data('itemidx-orig', thisLen);
3764
3765 thisTbody.appendChild(newRow);
3766 }, true);
3767 thisOptionContainer.appendChild(addRowButton);
3768
3769 (function(moduleID, optionKey) {
3770 $(thisTbody).dragsort({
3771 itemSelector: "tr",
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);
3780 },
3781 dragBetween: false,
3782 scrollContainer: this.RESConfigPanelOptions,
3783 placeHolderTemplate: "<tr><td>---</td></tr>",
3784 placeholderTemplate: "<tr><td>---</td></tr>",
3785 });
3786 })(moduleID, i);
3787 } else {
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);
3792 }
3793 var thisClear = document.createElement('div');
3794 thisClear.setAttribute('class','clear');
3795 thisOptionContainer.appendChild(thisClear);
3796 allOptionsContainer.appendChild(thisOptionContainer);
3797 }
3798 }
3799
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);
3805 } else {
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();
3813 } else {
3814 RESConsole.moduleOptionsScrim.classList.add('visible');
3815 $('#moduleOptionsSave').fadeOut();
3816 }
3817 allOptionsContainer.appendChild(this.moduleOptionsScrim);
3818 // console.log($(thisSaveButton).position());
3819 }
3820 },
3821 deleteOptionRow: function(e) {
3822 var thisRow = e.target.parentNode.parentNode;
3823 $(thisRow).remove();
3824 },
3825 saveCurrentModuleOptions: function(e) {
3826 e.preventDefault();
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...
3835 var optionName;
3836 if (inputs[i].getAttribute('type') === 'radio') {
3837 optionName = inputs[i].getAttribute('name');
3838 } else {
3839 optionName = inputs[i].getAttribute('id');
3840 }
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;
3848 }
3849 } else {
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')];
3855 } else {
3856 optionValue = inputs[i].value;
3857 }
3858 }
3859 if (typeof optionValue !== 'undefined') {
3860 RESUtils.setOption(moduleID, optionName, optionValue);
3861 }
3862 }
3863 }
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++) {
3880 var optionRow = [];
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;
3894 }
3895 } else {
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')];
3901 } else {
3902 optionValue = inputs[l].value;
3903 }
3904 }
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]))) {
3908 notAllBlank = true;
3909 }
3910 // optionRow[k] = optionValue;
3911 }
3912 optionRow.push(optionValue);
3913 }
3914 // just to be safe, added a check for optionRow !== null...
3915 if ((notAllBlank) && (optionRow !== null)) {
3916 optionMulti[optionRowCount] = optionRow;
3917 optionRowCount++;
3918 }
3919 }
3920 if (optionMulti == null) {
3921 optionMulti = [];
3922 }
3923 // ok, we've got all the rows... set the option.
3924 if (typeof optionValue !== 'undefined') {
3925 RESUtils.setOption(moduleID, optionName, optionMulti);
3926 }
3927 }
3928 }
3929 }
3930
3931 var statusEle = document.getElementById('moduleOptionsSaveStatus');
3932 if (statusEle) {
3933 $(statusEle).text('Options have been saved...');
3934 statusEle.setAttribute('style','display: block; opacity: 1');
3935 }
3936 RESUtils.fadeElementOut(statusEle, 0.1);
3937 if (moduleID === 'RESPro') RESStorage.removeItem('RESmodules.RESPro.lastAuthFailed');
3938 },
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> \
3947 </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> \
3953 <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>\
3956 </p> \
3957 <p> \
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> \
3960 </p> \
3961 <p></p> \
3962 <p></p> \
3963 <p> \
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&amp;w=117&amp;h=48&amp;style=white&amp;variant=text&amp;loc=en_US" type="image"/> \
3973 </form> \
3974 </p> \
3975 <p> \
3976 Or bitcoin: \
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." > \
3983 </form> \
3984 </p> \
3985 </div> \
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> \
3996 </div> \
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> \
4000 </div> \
4001 <div id="SearchRES" class="aboutPanel"></div> \
4002 </div> \
4003 ';
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');
4013
4014 var duration = (e.data && e.data.duration) || $(this).hasClass('active') ? 0 : 400;
4015 $(visiblePanel).fadeOut(duration, function () {
4016 $('#'+thisPanel).fadeIn();
4017 });
4018 });
4019 this.RESConsoleContent.appendChild(RESConsoleAboutPanel);
4020 },
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) {
4030 e.preventDefault();
4031 modules['RESPro'].configure();
4032 }, false);
4033 RESConsoleProPanel.appendChild(this.proSetupButton);
4034 /*
4035 this.proAuthButton = createElementWithID('div','RESProAuth');
4036 this.proAuthButton.setAttribute('class','RESButton');
4037 $(this.proAuthButton).html('Authenticate');
4038 this.proAuthButton.addEventListener('click', function(e) {
4039 e.preventDefault();
4040 modules['RESPro'].authenticate();
4041 }, false);
4042 RESConsoleProPanel.appendChild(this.proAuthButton);
4043 */
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) {
4048 e.preventDefault();
4049 // modules['RESPro'].savePrefs();
4050 modules['RESPro'].authenticate(modules['RESPro'].savePrefs());
4051 }, false);
4052 RESConsoleProPanel.appendChild(this.proSaveButton);
4053
4054 /*
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) {
4059 e.preventDefault();
4060 modules['RESPro'].saveModuleData('userTagger');
4061 }, false);
4062 RESConsoleProPanel.appendChild(this.proUserTaggerSaveButton);
4063 */
4064
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) {
4069 e.preventDefault();
4070 // modules['RESPro'].saveModuleData('saveComments');
4071 modules['RESPro'].authenticate(modules['RESPro'].saveModuleData('saveComments'));
4072 }, false);
4073 RESConsoleProPanel.appendChild(this.proSaveCommentsSaveButton);
4074
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) {
4079 e.preventDefault();
4080 // modules['RESPro'].saveModuleData('SubredditManager');
4081 modules['RESPro'].authenticate(modules['RESPro'].saveModuleData('subredditManager'));
4082 }, false);
4083 RESConsoleProPanel.appendChild(this.proSubredditManagerSaveButton);
4084
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) {
4089 e.preventDefault();
4090 // modules['RESPro'].getModuleData('saveComments');
4091 modules['RESPro'].authenticate(modules['RESPro'].getModuleData('saveComments'));
4092 }, false);
4093 RESConsoleProPanel.appendChild(this.proSaveCommentsGetButton);
4094
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) {
4099 e.preventDefault();
4100 // modules['RESPro'].getModuleData('SubredditManager');
4101 modules['RESPro'].authenticate(modules['RESPro'].getModuleData('subredditManager'));
4102 }, false);
4103 RESConsoleProPanel.appendChild(this.proSubredditManagerGetButton);
4104
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) {
4109 e.preventDefault();
4110 // modules['RESPro'].getPrefs();
4111 modules['RESPro'].authenticate(modules['RESPro'].getPrefs());
4112 }, false);
4113 RESConsoleProPanel.appendChild(this.proGetButton);
4114 this.RESConsoleContent.appendChild(RESConsoleProPanel);
4115 },
4116 open: function(moduleIdOrCategory) {
4117 var category, moduleID;
4118 if (moduleIdOrCategory === 'search') {
4119 moduleID = moduleIdOrCategory;
4120 category = 'About RES';
4121 } else {
4122 var module = modules[moduleIdOrCategory];
4123 moduleID = module && module.moduleID;
4124 category = module && module.category;
4125 }
4126 category = category || moduleIdOrCategory || this.categories[0];
4127 moduleID = moduleID || this.getModuleIDsByCategory(category)[0];
4128
4129
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);
4138
4139 this.isOpen = true;
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';
4144 }
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');
4150
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');
4155
4156 RESStorage.setItem('RESConsole.hasOpenedConsole', true);
4157
4158 document.body.addEventListener('keyup', RESConsole.handleEscapeKey, false);
4159 },
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)) {
4164 RESConsole.close();
4165 document.body.removeEventListener('keyup', RESConsole.handleEscapeKey, false);
4166 }
4167 },
4168 close: function() {
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';
4175 }
4176
4177 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'RESConsole');
4178
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;
4188 }
4189
4190 modules['settingsNavigation'].resetUrlHash();
4191 },
4192 menuClick: function(obj) {
4193 if (!obj) return;
4194
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);
4200 },
4201 openCategoryPanel: function(category) {
4202 // make all menu items look unselected
4203 $(RESConsole.RESMenuItems).removeClass('active');
4204
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];
4209
4210 if (thisCategory == category) return true;
4211 }).addClass('active');
4212
4213 // hide all console panels
4214 $(RESConsole.RESConsoleContent).find('.RESPanel').hide();
4215
4216 switch(category) {
4217 case 'Menu-About RES': // cruft
4218 case 'About RES':
4219 // show the about panel
4220 $(this.RESConsoleAboutPanel).show();
4221 break;
4222 case 'Menu-RES Pro': // cruft
4223 case 'RES Pro':
4224 // show the pro panel
4225 $(this.RESConsoleProPanel).show();
4226 break;
4227 default:
4228 // show the config panel for the given category
4229 $(this.RESConsoleConfigPanel).show();
4230 this.drawConfigPanelCategory(category);
4231 break;
4232 }
4233 }
4234 };
4235
4236
4237 /************************************************************************************************************
4238
4239 Creating your own module:
4240
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.
4250
4251 modules['myModule'] = {
4252 moduleID: 'myModule',
4253 moduleName: 'my module',
4254 category: 'CategoryName',
4255 options: {
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)
4259 // for example:
4260 defaultMessage: {
4261 type: 'text',
4262 value: 'this is default text',
4263 description: 'explanation of what this option is for'
4264 },
4265 doSpecialStuff: {
4266 type: 'boolean',
4267 value: false,
4268 description: 'explanation of what this option is for'
4269 }
4270 },
4271 description: 'This is my module!',
4272 isEnabled: function() {
4273 return RESConsole.getModulePrefs(this.moduleID);
4274 },
4275 include: [
4276 /^https?:\/\/([a-z]+)\.reddit\.com\/user\/[-\w\.]+/i,
4277 /^https?:\/\/([a-z]+)\.reddit\.com\/message\/comments\/[-\w\.]+/i
4278 ],
4279 isMatchURL: function() {
4280 return RESUtils.isMatchURL(this.moduleID);
4281 },
4282 go: function() {
4283 if ((this.isEnabled()) && (this.isMatchURL())) {
4284 // do stuff now!
4285 // this is where your code goes...
4286 }
4287 }
4288 }; // note: you NEED this semicolon at the end!
4289
4290 ************************************************************************************************************/
4291
4292
4293 modules['subRedditTagger'] = {
4294 moduleID: 'subRedditTagger',
4295 moduleName: 'Subreddit Tagger',
4296 category: 'Filters',
4297 options: {
4298 subReddits: {
4299 type: 'table',
4300 addRowText: '+add tag',
4301 fields: [
4302 { name: 'subreddit', type: 'text' },
4303 { name: 'doesntContain', type: 'text' },
4304 { name: 'tag', type: 'text' }
4305 ],
4306 value: [
4307 /*
4308 ['somebodymakethis','SMT','[SMT]'],
4309 ['pics','pic','[pic]']
4310 */
4311 ],
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.'
4313 }
4314 },
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);
4318 },
4319 include: [
4320 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
4321 ],
4322 isMatchURL: function() {
4323 return RESUtils.isMatchURL(this.moduleID);
4324 },
4325 go: function() {
4326 if ((this.isEnabled()) && (this.isMatchURL())) {
4327 this.checkForOldSettings();
4328 this.SRTDoesntContain = [];
4329 this.SRTTagWith = [];
4330 this.loadSRTRules();
4331
4332 RESUtils.watchForElement('siteTable', modules['subRedditTagger'].scanTitles);
4333 this.scanTitles();
4334
4335 }
4336 },
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];
4341 if (thisGetArray) {
4342 modules['subRedditTagger'].SRTDoesntContain[thisGetArray[0].toLowerCase()] = thisGetArray[1];
4343 modules['subRedditTagger'].SRTTagWith[thisGetArray[0].toLowerCase()] = thisGetArray[2];
4344 }
4345 }
4346 },
4347 scanTitles: function(obj) {
4348 var qs = '#siteTable > .thing > DIV.entry';
4349 if (obj) {
4350 qs = '.thing > DIV.entry';
4351 } else {
4352 obj = document;
4353 }
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+']';
4362 }
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());
4370 }
4371 }
4372 }
4373 }
4374 }
4375 },
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);
4384 subRedditCount++;
4385 }
4386 if (subRedditCount > 0) {
4387 RESUtils.setOption('subRedditTagger', 'subReddits', settingsCopy);
4388 }
4389 }
4390 };
4391
4392
4393 modules['uppersAndDowners'] = {
4394 moduleID: 'uppersAndDowners',
4395 moduleName: 'Uppers and Downers Enhanced',
4396 category: 'UI',
4397 options: {
4398 showSigns: {
4399 type: 'boolean',
4400 value: false,
4401 description: 'Show +/- signs next to upvote/downvote tallies.'
4402 },
4403 applyToLinks: {
4404 type: 'boolean',
4405 value: true,
4406 description: 'Uppers and Downers on links.'
4407 },
4408 postUpvoteStyle: {
4409 type: 'text',
4410 value: 'color:rgb(255, 139, 36); font-weight:normal;',
4411 description: 'CSS style for post upvotes'
4412 },
4413 postDownvoteStyle: {
4414 type: 'text',
4415 value: 'color:rgb(148, 148, 255); font-weight:normal;',
4416 description: 'CSS style for post upvotes'
4417 },
4418 commentUpvoteStyle: {
4419 type: 'text',
4420 value: 'color:rgb(255, 139, 36); font-weight:bold;',
4421 description: 'CSS style for comment upvotes'
4422 },
4423 commentDownvoteStyle: {
4424 type: 'text',
4425 value: 'color:rgb(148, 148, 255); font-weight:bold;',
4426 description: 'CSS style for comment upvotes'
4427 },
4428 forceVisible: {
4429 type: 'boolean',
4430 value: false,
4431 description: 'Force upvote/downvote counts to be visible (when subreddit CSS tries to hide them)'
4432 }
4433 },
4434 description: 'Displays up/down vote counts on comments.',
4435 isEnabled: function() {
4436 return RESConsole.getModulePrefs(this.moduleID);
4437 },
4438 include: [
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
4444 ],
4445 isMatchURL: function() {
4446 return RESUtils.isMatchURL(this.moduleID);
4447 },
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);
4455 }
4456 },
4457 go: function() {
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));
4463 }
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);
4474
4475 } else if ((RESUtils.pageType() === 'linklist') && (this.options.applyToLinks.value)) {
4476 this.linksWithMoos = [];
4477 this.applyUppersAndDownersToLinks();
4478 RESUtils.watchForElement('siteTable', modules['uppersAndDowners'].applyUppersAndDownersToLinks);
4479 }
4480 }
4481 },
4482 applyUppersAndDownersToComments: function(ele) {
4483 if (!ele) {
4484 ele = document.body;
4485 }
4486 if (ele.classList.contains('comment')) {
4487 modules['uppersAndDowners'].showUppersAndDownersOnComment(ele);
4488 } else if (ele.classList.contains('entry')) {
4489 modules['uppersAndDowners'].showUppersAndDownersOnComment(ele.parentNode);
4490 } else {
4491 var allComments = ele.querySelectorAll('div.comment');
4492 RESUtils.forEachChunked(allComments, 15, 1000, function(comment, i, array) {
4493 modules['uppersAndDowners'].showUppersAndDownersOnComment(comment);
4494 });
4495 }
4496 },
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) {
4503 thisPlus = '+';
4504 thisMinus = '-';
4505 } else {
4506 thisPlus = '';
4507 thisMinus = '';
4508 }
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');
4513
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);
4521 } else {
4522 $(thisTagline).after(upsAndDownsEle);
4523 }
4524 }
4525 } else {
4526 modules['uppersAndDowners'].showUppersAndDownersOnComment(linkList[i]);
4527 }
4528 }
4529
4530 },
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'))) {
4537 return;
4538 }
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
4545
4546
4547 if (modules['uppersAndDowners'].options.showSigns.value) {
4548 ups = '+'+ups;
4549 downs = '-'+downs;
4550 }
4551
4552 openparen = document.createTextNode(" (");
4553 frag.appendChild(openparen);
4554
4555 mooups = document.createElement("span");
4556 mooups.className = "res_comment_ups";
4557 voteUps = document.createTextNode(ups);
4558
4559 mooups.appendChild(voteUps);
4560 frag.appendChild(mooups);
4561
4562 pipe = document.createTextNode("|");
4563 tagline.appendChild(pipe);
4564
4565 moodowns = document.createElement("span");
4566 moodowns.className = "res_comment_downs";
4567
4568 voteDowns = document.createTextNode(downs);
4569 moodowns.appendChild(voteDowns);
4570
4571 frag.appendChild(moodowns);
4572
4573 closeparen = document.createTextNode(")");
4574 frag.appendChild(closeparen);
4575
4576 frag.appendChild(openparen);
4577 frag.appendChild(mooups);
4578 frag.appendChild(pipe);
4579 frag.appendChild(moodowns);
4580 frag.appendChild(closeparen);
4581
4582 tagline.appendChild(frag);
4583 },
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) {
4591 thisPlus = '+';
4592 thisMinus = '-';
4593 } else {
4594 thisPlus = '';
4595 thisMinus = '';
4596 }
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');
4600
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);
4608 } else {
4609 $(thisTagline).after(upsAndDownsEle);
4610 }
4611 }
4612 }
4613 }
4614 };
4615
4616 modules['keyboardNav'] = {
4617 moduleID: 'keyboardNav',
4618 moduleName: 'Keyboard Navigation',
4619 category: 'UI',
4620 options: {
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)
4624 // for example:
4625 focusBGColor: {
4626 type: 'text',
4627 value: '#F0F3FC',
4628 description: 'Background color of focused element'
4629 },
4630 focusBorder: {
4631 type: 'text',
4632 value: '',
4633 description: 'border style (e.g. 1px dashed gray) for focused element'
4634 },
4635 focusBGColorNight: {
4636 type: 'text',
4637 value: '#666',
4638 description: 'Background color of focused element in Night Mode'
4639 },
4640 focusFGColorNight: {
4641 type: 'text',
4642 value: '#DDD',
4643 description: 'Foreground color of focused element in Night Mode'
4644 },
4645 focusBorderNight: {
4646 type: 'text',
4647 value: '',
4648 description: 'border style (e.g. 1px dashed gray) for focused element'
4649 },
4650 autoSelectOnScroll: {
4651 type: 'boolean',
4652 value: false,
4653 description: 'Automatically select the topmost element for keyboard navigation on window scroll'
4654 },
4655 scrollOnExpando: {
4656 type: 'boolean',
4657 value: true,
4658 description: 'Scroll window to top of link when expando key is used (to keep pics etc in view)'
4659 },
4660 scrollStyle: {
4661 type: 'enum',
4662 values: [
4663 { name: 'directional', value: 'directional' },
4664 { name: 'page up/down', value: 'page' },
4665 { name: 'lock to top', value: 'top' }
4666 ],
4667 value: 'directional',
4668 description: 'When moving up/down with keynav, when and how should RES scroll the window?'
4669 },
4670 commentsLinkNumbers: {
4671 type: 'boolean',
4672 value: true,
4673 description: 'Assign number keys (e.g. [1]) to links within selected comment'
4674 },
4675 commentsLinkNumberPosition: {
4676 type: 'enum',
4677 values: [
4678 { name: 'Place on right', value: 'right' },
4679 { name: 'Place on left', value: 'left' }
4680 ],
4681 value: 'right',
4682 description: 'Which side commentsLinkNumbers are displayed'
4683 },
4684 commentsLinkNewTab: {
4685 type: 'boolean',
4686 value: true,
4687 description: 'Open number key links in a new tab'
4688 },
4689 clickFocus: {
4690 type: 'boolean',
4691 value: true,
4692 description: 'Move keyboard focus to a link or comment when clicked with the mouse'
4693 },
4694 onHideMoveDown: {
4695 type: 'boolean',
4696 value: true,
4697 description: 'After hiding a link, automatically select the next link'
4698 },
4699 onVoteMoveDown: {
4700 type: 'boolean',
4701 value: false,
4702 description: 'After voting on a link, automatically select the next link'
4703 },
4704 toggleHelp: {
4705 type: 'keycode',
4706 value: [191, false, false, true], // ? (note the true in the shift slot)
4707 description: 'Show help for keyboard shortcuts'
4708 },
4709 toggleCmdLine: {
4710 type: 'keycode',
4711 value: [190, false, false, false], // .
4712 description: 'Show/hide commandline box'
4713 },
4714 hide: {
4715 type: 'keycode',
4716 value: [72, false, false, false], // h
4717 description: 'Hide link'
4718 },
4719 moveUp: {
4720 type: 'keycode',
4721 value: [75, false, false, false], // k
4722 description: 'Move up (previous link or comment)'
4723 },
4724 moveDown: {
4725 type: 'keycode',
4726 value: [74, false, false, false], // j
4727 description: 'Move down (next link or comment)'
4728 },
4729 moveTop: {
4730 type: 'keycode',
4731 value: [75, false, false, true], // shift-k
4732 description: 'Move to top of list (on link pages)'
4733 },
4734 moveBottom: {
4735 type: 'keycode',
4736 value: [74, false, false, true], // shift-j
4737 description: 'Move to bottom of list (on link pages)'
4738 },
4739 moveUpSibling: {
4740 type: 'keycode',
4741 value: [75, false, false, true], // shift-k
4742 description: 'Move to previous sibling (in comments) - skips to previous sibling at the same depth.'
4743 },
4744 moveDownSibling: {
4745 type: 'keycode',
4746 value: [74, false, false, true], // shift-j
4747 description: 'Move to next sibling (in comments) - skips to next sibling at the same depth.'
4748 },
4749 moveUpThread: {
4750 type: 'keycode',
4751 value: [75, true, false, true], // shift-alt-k
4752 description: 'Move to the topmost comment of the previous thread (in comments).'
4753 },
4754 moveDownThread: {
4755 type: 'keycode',
4756 value: [74, true, false, true], // shift-alt-j
4757 description: 'Move to the topmost comment of the next thread (in comments).'
4758 },
4759 moveToTopComment: {
4760 type: 'keycode',
4761 value: [84, false, false, false], // t
4762 description: 'Move to the topmost comment of the current thread (in comments).'
4763 },
4764 moveToParent: {
4765 type: 'keycode',
4766 value: [80, false, false, false], // p
4767 description: 'Move to parent (in comments).'
4768 },
4769 showParents: {
4770 type: 'keycode',
4771 value: [80, false, false, true], // p
4772 description: 'Display parent comments.'
4773 },
4774 followLink: {
4775 type: 'keycode',
4776 value: [13, false, false, false], // enter
4777 description: 'Follow link (hold shift to open it in a new tab) (link pages only)'
4778 },
4779 followLinkNewTab: {
4780 type: 'keycode',
4781 value: [13, false, false, true], // shift-enter
4782 description: 'Follow link in new tab (link pages only)'
4783 },
4784 followLinkNewTabFocus: {
4785 type: 'boolean',
4786 value: true,
4787 description: 'When following a link in new tab - focus the tab?'
4788 },
4789 toggleExpando: {
4790 type: 'keycode',
4791 value: [88, false, false, false], // x
4792 description: 'Toggle expando (image/text/video) (link pages only)'
4793 },
4794 imageSizeUp: {
4795 type: 'keycode',
4796 value: [187, false, false, false],
4797 description: 'Increase the size of image(s) in the highlighted post area'
4798 },
4799 imageSizeDown: {
4800 type: 'keycode',
4801 value: [189, false, false, false],
4802 description: 'Increase the size of image(s) in the highlighted post area'
4803 },
4804 imageSizeUpFine: {
4805 type: 'keycode',
4806 value: [187, false, false, true],
4807 description: 'Increase the size of image(s) in the highlighted post area (finer control)'
4808 },
4809 imageSizeDownFine: {
4810 type: 'keycode',
4811 value: [189, false, false, true],
4812 description: 'Increase the size of image(s) in the highlighted post area (finer control)'
4813 },
4814 previousGalleryImage: {
4815 type: 'keycode',
4816 value: [219, false, false, false], //[
4817 description: 'View the previous image of an inline gallery.'
4818 },
4819 nextGalleryImage: {
4820 type: 'keycode',
4821 value: [221, false, false, false], //]
4822 description: 'View the next image of an inline gallery.'
4823 },
4824 toggleViewImages: {
4825 type: 'keycode',
4826 value: [88, false, false, true], // shift-x
4827 description: 'Toggle "view images" button'
4828 },
4829 toggleChildren: {
4830 type: 'keycode',
4831 value: [13, false, false, false], // enter
4832 description: 'Expand/collapse comments (comments pages only)'
4833 },
4834 followComments: {
4835 type: 'keycode',
4836 value: [67, false, false, false], // c
4837 description: 'View comments for link (shift opens them in a new tab)'
4838 },
4839 followCommentsNewTab: {
4840 type: 'keycode',
4841 value: [67, false, false, true], // shift-c
4842 description: 'View comments for link in a new tab'
4843 },
4844 followLinkAndCommentsNewTab: {
4845 type: 'keycode',
4846 value: [76, false, false, false], // l
4847 description: 'View link and comments in new tabs'
4848 },
4849 followLinkAndCommentsNewTabBG: {
4850 type: 'keycode',
4851 value: [76, false, false, true], // shift-l
4852 description: 'View link and comments in new background tabs'
4853 },
4854 upVote: {
4855 type: 'keycode',
4856 value: [65, false, false, false], // a
4857 description: 'Upvote selected link or comment'
4858 },
4859 downVote: {
4860 type: 'keycode',
4861 value: [90, false, false, false], // z
4862 description: 'Downvote selected link or comment'
4863 },
4864 save: {
4865 type: 'keycode',
4866 value: [83, false, false, false], // s
4867 description: 'Save the current link'
4868 },
4869 reply: {
4870 type: 'keycode',
4871 value: [82, false, false, false], // r
4872 description: 'Reply to current comment (comment pages only)'
4873 },
4874 openBigEditor: {
4875 type: 'keycode',
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)'
4878 },
4879 followSubreddit: {
4880 type: 'keycode',
4881 value: [82, false, false, false], // r
4882 description: 'Go to subreddit of selected link (link pages only)'
4883 },
4884 followSubredditNewTab: {
4885 type: 'keycode',
4886 value: [82, false, false, true], // shift-r
4887 description: 'Go to subreddit of selected link in a new tab (link pages only)'
4888 },
4889 inbox: {
4890 type: 'keycode',
4891 value: [73, false, false, false], // i
4892 description: 'Go to inbox'
4893 },
4894 inboxNewTab: {
4895 type: 'keycode',
4896 value: [73, false, false, true], // shift+i
4897 description: 'Go to inbox in a new tab'
4898 },
4899 profile: {
4900 type: 'keycode',
4901 value: [85, false, false, false], // u
4902 description: 'Go to profile'
4903 },
4904 profileNewTab: {
4905 type: 'keycode',
4906 value: [85, false, false, true], // shift+u
4907 description: 'Go to profile in a new tab'
4908 },
4909 frontPage: {
4910 type: 'keycode',
4911 value: [70, false, false, false], // f
4912 description: 'Go to front page'
4913 },
4914 subredditFrontPage: {
4915 type: 'keycode',
4916 value: [70, false, false, true], // shift-f
4917 description: 'Go to subreddit front page'
4918 },
4919 nextPage: {
4920 type: 'keycode',
4921 value: [78, false, false, false], // n
4922 description: 'Go to next page (link list pages only)'
4923 },
4924 prevPage: {
4925 type: 'keycode',
4926 value: [80, false, false, false], // p
4927 description: 'Go to prev page (link list pages only)'
4928 },
4929 link1: {
4930 type: 'keycode',
4931 value: [49, false, false, false], // 1
4932 description: 'Open first link within comment.',
4933 noconfig: true
4934 },
4935 link2: {
4936 type: 'keycode',
4937 value: [50, false, false, false], // 2
4938 description: 'Open link #2 within comment.',
4939 noconfig: true
4940 },
4941 link3: {
4942 type: 'keycode',
4943 value: [51, false, false, false], // 3
4944 description: 'Open link #3 within comment.',
4945 noconfig: true
4946 },
4947 link4: {
4948 type: 'keycode',
4949 value: [52, false, false, false], // 4
4950 description: 'Open link #4 within comment.',
4951 noconfig: true
4952 },
4953 link5: {
4954 type: 'keycode',
4955 value: [53, false, false, false], // 5
4956 description: 'Open link #5 within comment.',
4957 noconfig: true
4958 },
4959 link6: {
4960 type: 'keycode',
4961 value: [54, false, false, false], // 6
4962 description: 'Open link #6 within comment.',
4963 noconfig: true
4964 },
4965 link7: {
4966 type: 'keycode',
4967 value: [55, false, false, false], // 7
4968 description: 'Open link #7 within comment.',
4969 noconfig: true
4970 },
4971 link8: {
4972 type: 'keycode',
4973 value: [56, false, false, false], // 8
4974 description: 'Open link #8 within comment.',
4975 noconfig: true
4976 },
4977 link9: {
4978 type: 'keycode',
4979 value: [57, false, false, false], // 9
4980 description: 'Open link #9 within comment.',
4981 noconfig: true
4982 },
4983 link10: {
4984 type: 'keycode',
4985 value: [48, false, false, false], // 0
4986 description: 'Open link #10 within comment.',
4987 noconfig: true
4988 }
4989 },
4990 description: 'Keyboard navigation for reddit!',
4991 isEnabled: function() {
4992 return RESConsole.getModulePrefs(this.moduleID);
4993 },
4994 include: [
4995 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
4996 ],
4997 isMatchURL: function() {
4998 return RESUtils.isMatchURL(this.moduleID);
4999 },
5000 beforeLoad: function() {
5001 if ((this.isEnabled()) && (this.isMatchURL())) {
5002 var focusFGColorNight, focusBGColor, focusBGColorNight;
5003 if (typeof this.options.focusBGColor === 'undefined') {
5004 focusBGColor = '#F0F3FC';
5005 } else {
5006 focusBGColor = this.options.focusBGColor.value;
5007 }
5008 var borderType = 'outline';
5009 if (BrowserDetect.isOpera()) borderType = 'border';
5010 if (typeof this.options.focusBorder === 'undefined') {
5011 focusBorder = '';
5012 } else {
5013 focusBorder = borderType+': ' + this.options.focusBorder.value + ';';
5014 }
5015 if (!(this.options.focusBGColorNight.value)) {
5016 focusBGColorNight = '#666';
5017 } else {
5018 focusBGColorNight = this.options.focusBGColorNight.value;
5019 }
5020 if (!(this.options.focusFGColorNight.value)) {
5021 focusFGColorNight = '#DDD';
5022 } else {
5023 focusFGColorNight = this.options.focusFGColorNight.value;
5024 }
5025 if (typeof this.options.focusBorderNight === 'undefined') {
5026 focusBorderNight = '';
5027 } else {
5028 focusBorderNight = borderType+': ' + this.options.focusBorderNight.value + ';';
5029 }
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; } \
5034
5035 // why !important on .RES-keyNav-activeElement? Because some subreddits are unfortunately using !important for no good reason on .entry divs...
5036 RESUtils.addCSS(' \
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; } \
5054 ');
5055 }
5056 },
5057 go: function() {
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],
5070 updated: now
5071 };
5072 RESStorage.removeItem(idx);
5073 }
5074 }
5075 this.keyboardNavLastIndexCache.lastScan = now;
5076 RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
5077 } else {
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];
5083 }
5084 }
5085 this.keyboardNavLastIndexCache.lastScan = now;
5086 RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
5087 }
5088 }
5089
5090 if (this.options.autoSelectOnScroll.value) {
5091 window.addEventListener('scroll', modules['keyboardNav'].handleScroll, false);
5092 }
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));
5097 }
5098 this.drawHelp();
5099 this.attachCommandLineWidget();
5100 window.addEventListener('keydown', function(e) {
5101 // console.log(e.keyCode);
5102 modules['keyboardNav'].handleKeyPress(e);
5103 }, true);
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);
5108 } else {
5109 RESUtils.watchForElement('siteTable', modules['keyboardNav'].scanPageForNewKeyboardLinks);
5110 }
5111 }
5112 },
5113 scanPageForNewKeyboardLinks: function() {
5114 modules['keyboardNav'].scanPageForKeyboardLinks(true);
5115 },
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] = {};
5122 }
5123 var now = new Date().getTime();
5124 this.keyboardNavLastIndexCache[trimLoc] = {
5125 index: this.activeIndex,
5126 updated: now
5127 };
5128 RESStorage.setItem('RESmodules.keyboardNavLastIndex', JSON.stringify(this.keyboardNavLastIndexCache));
5129 },
5130 handleScroll: function(e) {
5131 if (modules['keyboardNav'].scrollTimer) clearTimeout(modules['keyboardNav'].scrollTimer);
5132 modules['keyboardNav'].scrollTimer = setTimeout(modules['keyboardNav'].handleScrollAfterTimer, 300);
5133 },
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]);
5141 break;
5142 }
5143 }
5144 }
5145 },
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);
5152 }, false);
5153 this.commandLineInput.addEventListener('keyup', function(e) {
5154 if (e.keyCode === 27) {
5155 // close prompt.
5156 modules['keyboardNav'].toggleCmdLine(false);
5157 } else {
5158 // auto suggest?
5159 modules['keyboardNav'].cmdLineHelper(e.target.value);
5160 }
5161 }, false);
5162 this.commandLineInputTip = createElementWithID('div','keyCommandInputTip');
5163 this.commandLineInputError = createElementWithID('div','keyCommandInputError');
5164
5165 /*
5166 this.commandLineSubmit = createElementWithID('input','keyCommandInput');
5167 this.commandLineSubmit.setAttribute('type','submit');
5168 this.commandLineSubmit.setAttribute('value','go');
5169 */
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);
5180
5181 },
5182 cmdLineHelper: function (val) {
5183 var splitWords = val.split(' '),
5184 command = splitWords[0],
5185 str, srString;
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;
5211 }
5212 str = 'tag user ' + this.cmdLineTagUsername;
5213 if (val) {
5214 str += ' as: ' + val;
5215 }
5216 this.cmdLineShowTip(str);
5217 } else if (command === 'user') {
5218 str = 'go to profile';
5219 if (val) {
5220 str += ' for: ' + val;
5221 }
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';
5235 if (val) {
5236 str += ' for: ' + val;
5237 } else {
5238 if (RESUtils.currentSubreddit()) {
5239 str += ' for: ' + RESUtils.currentSubreddit();
5240 }
5241 }
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:';
5249 str += '<ul>';
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>';
5266 str += '</ul>';
5267 this.cmdLineShowTip(str);
5268 } else {
5269 this.cmdLineShowTip('');
5270 }
5271 },
5272 cmdLineShowTip: function(str) {
5273 $(this.commandLineInputTip).html(str);
5274 },
5275 cmdLineShowError: function(str) {
5276 $(this.commandLineInputError).html(str);
5277 },
5278 toggleCmdLine: function(force) {
5279 var open = ((force == null || force) && (this.commandLineWidget.style.display !== 'block'));
5280 delete this.cmdLineTagUsername;
5281 if (open) {
5282 this.cmdLineShowError('');
5283 this.commandLineWidget.style.display = 'block';
5284 setTimeout(function() {
5285 modules['keyboardNav'].commandLineInput.focus();
5286 }, 20);
5287 this.commandLineInput.value = '';
5288 } else {
5289 modules['keyboardNav'].commandLineInput.blur();
5290 this.commandLineWidget.style.display = 'none';
5291 }
5292 modules['styleTweaks'].setSRStyleToggleVisibility(!open, 'cmdline');
5293 },
5294 cmdLineSubmit: function(e) {
5295 e.preventDefault();
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) {
5311 // sort...
5312 theInput = theInput.slice(1);
5313 switch (theInput) {
5314 case 'n':
5315 theInput = 'new';
5316 break;
5317 case 't':
5318 theInput = 'top';
5319 break;
5320 case 'h':
5321 theInput = 'hot';
5322 break;
5323 case 'c':
5324 theInput = 'controversial';
5325 break;
5326 }
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;
5333 } else {
5334 location.href = '/'+theInput;
5335 }
5336 } else {
5337 modules['keyboardNav'].cmdLineShowError('invalid sort command - must be [n]ew, [t]op, [h]ot or [c]ontroversial');
5338 return false;
5339 }
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();
5349 }
5350 } else {
5351 var splitWords = theInput.split(' ');
5352 var command = splitWords[0];
5353 splitWords.splice(0,1);
5354 var val = splitWords.join(' ');
5355 switch (command) {
5356 case 'tag':
5357 var searchArea = modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex];
5358 var tagLink = searchArea.querySelector('a.userTagLink');
5359 if (tagLink) {
5360 RESUtils.click(tagLink);
5361 setTimeout(function() {
5362 if (val !== '') {
5363 document.getElementById('userTaggerTag').value = val;
5364 }
5365 }, 20);
5366 }
5367 break;
5368 case 'sw':
5369 // switch accounts (username is required)
5370 if (val.length <= 1) {
5371 modules['keyboardNav'].cmdLineShowError('No username specified.');
5372 return false;
5373 } else {
5374 // first make sure the account exists...
5375 var accounts = modules['accountSwitcher'].options.accounts.value;
5376 var found = false;
5377 for (var i=0, len=accounts.length; i<len; i++) {
5378 var thisPair = accounts[i];
5379 if (thisPair[0] == val) {
5380 found = true;
5381 }
5382 }
5383 if (found) {
5384 modules['accountSwitcher'].switchTo(val);
5385 } else {
5386 modules['keyboardNav'].cmdLineShowError('No such username in accountSwitcher.');
5387 return false;
5388 }
5389 }
5390 break;
5391 case 'user':
5392 // view profile for username (username is required)
5393 if (val.length <= 1) {
5394 modules['keyboardNav'].cmdLineShowError('No username specified.');
5395 return false;
5396 } else {
5397 location.href = '/user/' + val;
5398 }
5399 break;
5400 case 'userinfo':
5401 // view JSON data for username (username is required)
5402 if (val.length <= 1) {
5403 modules['keyboardNav'].cmdLineShowError('No username specified.');
5404 return false;
5405 } else {
5406 GM_xmlhttpRequest({
5407 method: "GET",
5408 url: location.protocol + "//"+location.hostname+"/user/" + val + "/about.json?app=res",
5409 onload: function(response) {
5410 alert(response.responseText);
5411 }
5412 });
5413 }
5414 break;
5415 case 'userbadge':
5416 // get CSS code for a badge for username (username is required)
5417 if (val.length <= 1) {
5418 modules['keyboardNav'].cmdLineShowError('No username specified.');
5419 return false;
5420 } else {
5421 GM_xmlhttpRequest({
5422 method: "GET",
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';
5427 alert(css);
5428 }
5429 });
5430 }
5431 break;
5432 case 'm':
5433 // go to inbox
5434 location.href = '/message/inbox/';
5435 break;
5436 case 'mm':
5437 // go to mod mail
5438 location.href = '/message/moderator/';
5439 break;
5440 case 'ls':
5441 // toggle lightSwitch
5442 RESUtils.click(modules['styleTweaks'].lightSwitch);
5443 break;
5444 case 'nsfw':
5445 var toggle;
5446 switch (val && val.toLowerCase()) {
5447 case 'on':
5448 toggle = true;
5449 break;
5450 case 'off':
5451 toggle = false;
5452 break;
5453 }
5454 modules['filteReddit'].toggleNsfwFilter(toggle, true);
5455 break;
5456 case 'srstyle':
5457 // toggle subreddit style
5458 var sr;
5459 var toggleText;
5460 splitWords = val.split(' ');
5461 if (splitWords.length === 2) {
5462 sr = splitWords[0];
5463 toggleText = splitWords[1];
5464 } else {
5465 sr = RESUtils.currentSubreddit();
5466 toggleText = splitWords[0];
5467 }
5468 if (!sr) {
5469 modules['keyboardNav'].cmdLineShowError('No subreddit specified.');
5470 return false;
5471 }
5472 if (toggleText === 'on') {
5473 toggle = true;
5474 } else if (toggleText === 'off') {
5475 toggle = false;
5476 } else {
5477 modules['keyboardNav'].cmdLineShowError('You must specify "on" or "off".');
5478 return false;
5479 }
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
5486 }, 4000);
5487 break;
5488 case 'notification':
5489 // test notification
5490 RESUtils.notification(val, 4000);
5491 break;
5492 case 'search':
5493 modules['settingsNavigation'].search(val);
5494 break;
5495 case 'RESStorage':
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]"');
5500 } else {
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(' ');
5506 }
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);
5514 if (textArea) {
5515 var value = textArea.value;
5516 RESStorage.setItem(key, value);
5517 }
5518 });
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>');
5525 } else {
5526 modules['keyboardNav'].cmdLineShowError('You must specify either "get [key]" or "set [key] [value]"');
5527 }
5528 }
5529 break;
5530 case 'XHRCache':
5531 splitWords = val.split(' ');
5532 if (splitWords.length < 1) {
5533 modules['keyboardNav'].cmdLineShowError('Operation required [clear]');
5534 } else {
5535 switch (splitWords[0]) {
5536 case 'clear':
5537 RESUtils.xhrCache('clear');
5538 break;
5539 default:
5540 modules['keyboardNav'].cmdLineShowError('The only accepted operation is <tt>clear</tt>');
5541 break;
5542 }
5543 }
5544 break;
5545 case '?':
5546 // user is already looking at help... do nothing.
5547 return false;
5548 break;
5549 default:
5550 modules['keyboardNav'].cmdLineShowError('unknown command - type ? for help');
5551 return false;
5552 break;
5553 }
5554 }
5555 // hide the commandline tool...
5556 modules['keyboardNav'].toggleCmdLine(false);
5557 },
5558 scanPageForKeyboardLinks: function(isNew) {
5559 if (typeof isNew === 'undefined') {
5560 isNew = false;
5561 }
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) {
5565 case 'linklist':
5566 case 'profile':
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];
5573 }
5574 if (siteTable) {
5575 this.keyboardLinks = document.body.querySelectorAll('div.linklisting .entry');
5576 if (!isNew) {
5577 if ((this.keyboardNavLastIndexCache[location.href]) && (this.keyboardNavLastIndexCache[location.href].index > 0)) {
5578 this.activeIndex = this.keyboardNavLastIndexCache[location.href].index;
5579 } else {
5580 this.activeIndex = 0;
5581 }
5582 if ((this.keyboardNavLastIndexCache[location.href]) && (this.keyboardNavLastIndexCache[location.href].index >= this.keyboardLinks.length)) {
5583 this.activeIndex = 0;
5584 }
5585 }
5586 }
5587 break;
5588 case 'comments':
5589 // get all links into an array...
5590 this.keyboardLinks = document.body.querySelectorAll('#siteTable .entry, div.content > div.commentarea .entry');
5591 if (!(isNew)) {
5592 this.activeIndex = 0;
5593 }
5594 break;
5595 case 'inbox':
5596 var siteTable = document.querySelector('#siteTable');
5597 if (siteTable) {
5598 this.keyboardLinks = siteTable.querySelectorAll('.entry');
5599 this.activeIndex = 0;
5600 }
5601 break;
5602 }
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]);
5614 }
5615 }).bind(this.keyboardLinks[i]), true);
5616 }
5617 this.keyFocus(this.keyboardLinks[this.activeIndex]);
5618 }
5619 },
5620 recentKey: function() {
5621 modules['keyboardNav'].recentKeyPress = true;
5622 clearTimeout(modules['keyboardNav'].recentKey);
5623 modules['keyboardNav'].recentKeyTimer = setTimeout(function() {
5624 modules['keyboardNav'].recentKeyPress = false;
5625 }, 1000);
5626 },
5627 keyFocus: function(obj) {
5628 if ((typeof obj !== 'undefined') && (obj.classList.contains('RES-keyNav-activeElement'))) {
5629 return false;
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')) {
5634 this.setKeyIndex();
5635 }
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');
5645 annotationCount++;
5646 $(annotation).text('['+annotationCount+'] ');
5647 annotation.title = 'press '+annotationCount+' to open link';
5648 annotation.classList.add('keyNavAnnotation');
5649 /*
5650 if (!(hasClass(links[i],'hasListener'))) {
5651 addClass(links[i],'hasListener');
5652 links[i].addEventListener('click', modules['keyboardNav'].handleKeyLink, true);
5653 }
5654 */
5655 if (modules['keyboardNav'].options.commentsLinkNumberPosition.value === 'right') {
5656 insertAfter(links[i], annotation);
5657 } else {
5658 links[i].parentNode.insertBefore(annotation, links[i]);
5659 }
5660 }
5661 }
5662 }
5663 }
5664 },
5665 handleKeyLink: function(link) {
5666 var button = 0;
5667 if ((modules['keyboardNav'].options.commentsLinkNewTab.value) || e.ctrlKey) {
5668 button = 1;
5669 }
5670 if (link.classList.contains('toggleImage')) {
5671 RESUtils.click(link);
5672 return false;
5673 }
5674 var thisURL = link.getAttribute('href'),
5675 isLocalToPage = (thisURL.indexOf('reddit') !== -1) && (thisURL.indexOf('comments') !== -1) && (thisURL.indexOf('#') !== -1);
5676 if ((!isLocalToPage) && (button === 1)) {
5677 var thisJSON;
5678 if (BrowserDetect.isChrome()) {
5679 thisJSON = {
5680 requestType: 'keyboardNav',
5681 linkURL: thisURL,
5682 button: button
5683 };
5684 chrome.extension.sendMessage(thisJSON);
5685 } else if (BrowserDetect.isSafari()) {
5686 thisJSON = {
5687 requestType: 'keyboardNav',
5688 linkURL: thisURL,
5689 button: button
5690 };
5691 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
5692 } else if (BrowserDetect.isOpera()) {
5693 thisJSON = {
5694 requestType: 'keyboardNav',
5695 linkURL: thisURL,
5696 button: button
5697 };
5698 opera.extension.postMessage(JSON.stringify(thisJSON));
5699 } else if (BrowserDetect.isFirefox()) {
5700 thisJSON = {
5701 requestType: 'keyboardNav',
5702 linkURL: thisURL,
5703 button: button
5704 };
5705 self.postMessage(thisJSON);
5706 } else {
5707 window.open(this.getAttribute('href'));
5708 }
5709 } else {
5710 location.href = this.getAttribute('href');
5711 }
5712 },
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]);
5719 }
5720 }
5721 RESUtils.hover.close(false);
5722 },
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);
5750 }
5751 }
5752 helpTable.appendChild(helpTableBody);
5753 document.body.appendChild(thisHelp);
5754 },
5755 getNiceKeyCode: function(optionKey) {
5756 var keyCodeArray = this.options[optionKey].value;
5757 if (!keyCodeArray) return;
5758
5759 if (typeof keyCodeArray === 'string') {
5760 keyCodeArray = parseInt(keyCodeArray);
5761 }
5762 if (typeof keyCodeArray === 'number') {
5763 keyCodeArray = [keyCodeArray, false, false, false, false];
5764 }
5765 var niceKeyCode = RESUtils.niceKeyCode(keyCodeArray);
5766 return niceKeyCode;
5767 },
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) {
5774 case 'linklist':
5775 case 'profile':
5776 switch(true) {
5777 case checkKeysForEvent(e, this.options.moveUp.value):
5778 this.moveUp();
5779 break;
5780 case checkKeysForEvent(e, this.options.moveDown.value):
5781 this.moveDown();
5782 break;
5783 case checkKeysForEvent(e, this.options.moveTop.value):
5784 this.moveTop();
5785 break;
5786 case checkKeysForEvent(e, this.options.moveBottom.value):
5787 this.moveBottom();
5788 break;
5789 case checkKeysForEvent(e, this.options.followLink.value):
5790 this.followLink();
5791 break;
5792 case checkKeysForEvent(e, this.options.followLinkNewTab.value):
5793 e.preventDefault();
5794 this.followLink(true);
5795 break;
5796 case checkKeysForEvent(e, this.options.followComments.value):
5797 this.followComments();
5798 break;
5799 case checkKeysForEvent(e, this.options.followCommentsNewTab.value):
5800 e.preventDefault();
5801 this.followComments(true);
5802 break;
5803 case checkKeysForEvent(e, this.options.toggleExpando.value):
5804 this.toggleExpando();
5805 break;
5806 case checkKeysForEvent(e, this.options.imageSizeUp.value):
5807 this.imageSizeUp();
5808 break;
5809 case checkKeysForEvent(e, this.options.imageSizeDown.value):
5810 this.imageSizeDown();
5811 break;
5812 case checkKeysForEvent(e, this.options.imageSizeUpFine.value):
5813 this.imageSizeUp(true);
5814 break;
5815 case checkKeysForEvent(e, this.options.imageSizeDownFine.value):
5816 this.imageSizeDown(true);
5817 break;
5818 case checkKeysForEvent(e, this.options.previousGalleryImage.value):
5819 this.previousGalleryImage();
5820 break;
5821 case checkKeysForEvent(e, this.options.nextGalleryImage.value):
5822 this.nextGalleryImage();
5823 break;
5824 case checkKeysForEvent(e, this.options.toggleViewImages.value):
5825 this.toggleViewImages();
5826 break;
5827 case checkKeysForEvent(e, this.options.followLinkAndCommentsNewTab.value):
5828 e.preventDefault();
5829 this.followLinkAndComments();
5830 break;
5831 case checkKeysForEvent(e, this.options.followLinkAndCommentsNewTabBG.value):
5832 e.preventDefault();
5833 this.followLinkAndComments(true);
5834 break;
5835 case checkKeysForEvent(e, this.options.upVote.value):
5836 this.upVote(true);
5837 break;
5838 case checkKeysForEvent(e, this.options.downVote.value):
5839 this.downVote(true);
5840 break;
5841 case checkKeysForEvent(e, this.options.save.value):
5842 this.saveLink();
5843 break;
5844 case checkKeysForEvent(e, this.options.inbox.value):
5845 e.preventDefault();
5846 this.inbox();
5847 break;
5848 case checkKeysForEvent(e, this.options.inboxNewTab.value):
5849 e.preventDefault();
5850 this.inbox(true);
5851 break;
5852 case checkKeysForEvent(e, this.options.profile.value):
5853 e.preventDefault();
5854 this.profile();
5855 break;
5856 case checkKeysForEvent(e, this.options.profileNewTab.value):
5857 e.preventDefault();
5858 this.profile(true);
5859 break;
5860 case checkKeysForEvent(e, this.options.frontPage.value):
5861 e.preventDefault();
5862 this.frontPage();
5863 break;
5864 case checkKeysForEvent(e, this.options.nextPage.value):
5865 e.preventDefault();
5866 this.nextPage();
5867 break;
5868 case checkKeysForEvent(e, this.options.prevPage.value):
5869 e.preventDefault();
5870 this.prevPage();
5871 break;
5872 case checkKeysForEvent(e, this.options.toggleHelp.value):
5873 this.toggleHelp();
5874 break;
5875 case checkKeysForEvent(e, this.options.toggleCmdLine.value):
5876 this.toggleCmdLine();
5877 break;
5878 case checkKeysForEvent(e, this.options.hide.value):
5879 this.hide();
5880 break;
5881 case checkKeysForEvent(e, this.options.followSubreddit.value):
5882 this.followSubreddit();
5883 break;
5884 case checkKeysForEvent(e, this.options.followSubredditNewTab.value):
5885 this.followSubreddit(true);
5886 break;
5887 default:
5888 // do nothing. unrecognized key.
5889 break;
5890 }
5891 break;
5892 case 'comments':
5893 switch(true) {
5894 case checkKeysForEvent(e, this.options.toggleHelp.value):
5895 this.toggleHelp();
5896 break;
5897 case checkKeysForEvent(e, this.options.toggleCmdLine.value):
5898 this.toggleCmdLine();
5899 break;
5900 case checkKeysForEvent(e, this.options.moveUp.value):
5901 this.moveUp();
5902 break;
5903 case checkKeysForEvent(e, this.options.moveDown.value):
5904 this.moveDown();
5905 break;
5906 case checkKeysForEvent(e, this.options.moveUpSibling.value):
5907 this.moveUpSibling();
5908 break;
5909 case checkKeysForEvent(e, this.options.moveDownSibling.value):
5910 this.moveDownSibling();
5911 break;
5912 case checkKeysForEvent(e, this.options.moveUpThread.value):
5913 this.moveUpThread();
5914 break;
5915 case checkKeysForEvent(e, this.options.moveDownThread.value):
5916 this.moveDownThread();
5917 break;
5918 case checkKeysForEvent(e, this.options.moveToTopComment.value):
5919 this.moveToTopComment();
5920 break;
5921 case checkKeysForEvent(e, this.options.moveToParent.value):
5922 this.moveToParent();
5923 break;
5924 case checkKeysForEvent(e, this.options.showParents.value):
5925 this.showParents();
5926 break;
5927 case checkKeysForEvent(e, this.options.toggleChildren.value):
5928 this.toggleChildren();
5929 break;
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) {
5933 e.preventDefault();
5934 this.followLink(true);
5935 }
5936 break;
5937 case checkKeysForEvent(e, this.options.save.value):
5938 if (this.activeIndex === 0) {
5939 this.saveLink();
5940 } else {
5941 this.saveComment();
5942 }
5943 break;
5944 case checkKeysForEvent(e, this.options.toggleExpando.value):
5945 this.toggleAllExpandos();
5946 break;
5947 case checkKeysForEvent(e, this.options.previousGalleryImage.value):
5948 this.previousGalleryImage();
5949 break;
5950 case checkKeysForEvent(e, this.options.imageSizeUp.value):
5951 this.imageSizeUp();
5952 break;
5953 case checkKeysForEvent(e, this.options.imageSizeDown.value):
5954 this.imageSizeDown();
5955 break;
5956 case checkKeysForEvent(e, this.options.imageSizeUpFine.value):
5957 this.imageSizeUp(true);
5958 break;
5959 case checkKeysForEvent(e, this.options.imageSizeDownFine.value):
5960 this.imageSizeDown(true);
5961 break;
5962 case checkKeysForEvent(e, this.options.nextGalleryImage.value):
5963 this.nextGalleryImage();
5964 break;
5965 case checkKeysForEvent(e, this.options.toggleViewImages.value):
5966 this.toggleViewImages();
5967 break;
5968 case checkKeysForEvent(e, this.options.upVote.value):
5969 this.upVote();
5970 break;
5971 case checkKeysForEvent(e, this.options.downVote.value):
5972 this.downVote();
5973 break;
5974 case checkKeysForEvent(e, this.options.reply.value):
5975 e.preventDefault();
5976 this.reply();
5977 break;
5978 case checkKeysForEvent(e, this.options.inbox.value):
5979 e.preventDefault();
5980 this.inbox();
5981 break;
5982 case checkKeysForEvent(e, this.options.inboxNewTab.value):
5983 e.preventDefault();
5984 this.inbox(true);
5985 break;
5986 case checkKeysForEvent(e, this.options.profile.value):
5987 e.preventDefault();
5988 this.profile();
5989 break;
5990 case checkKeysForEvent(e, this.options.profileNewTab.value):
5991 e.preventDefault();
5992 this.profile(true);
5993 break;
5994 case checkKeysForEvent(e, this.options.frontPage.value):
5995 e.preventDefault();
5996 this.frontPage();
5997 break;
5998 case checkKeysForEvent(e, this.options.subredditFrontPage.value):
5999 e.preventDefault();
6000 this.frontPage(true);
6001 break;
6002 case checkKeysForEvent(e, this.options.link1.value):
6003 e.preventDefault();
6004 this.commentLink(0);
6005 break;
6006 case checkKeysForEvent(e, this.options.link2.value):
6007 e.preventDefault();
6008 this.commentLink(1);
6009 break;
6010 case checkKeysForEvent(e, this.options.link3.value):
6011 e.preventDefault();
6012 this.commentLink(2);
6013 break;
6014 case checkKeysForEvent(e, this.options.link4.value):
6015 e.preventDefault();
6016 this.commentLink(3);
6017 break;
6018 case checkKeysForEvent(e, this.options.link5.value):
6019 e.preventDefault();
6020 this.commentLink(4);
6021 break;
6022 case checkKeysForEvent(e, this.options.link6.value):
6023 e.preventDefault();
6024 this.commentLink(5);
6025 break;
6026 case checkKeysForEvent(e, this.options.link7.value):
6027 e.preventDefault();
6028 this.commentLink(6);
6029 break;
6030 case checkKeysForEvent(e, this.options.link8.value):
6031 e.preventDefault();
6032 this.commentLink(7);
6033 break;
6034 case checkKeysForEvent(e, this.options.link9.value):
6035 e.preventDefault();
6036 this.commentLink(8);
6037 break;
6038 case checkKeysForEvent(e, this.options.link10.value):
6039 e.preventDefault();
6040 this.commentLink(9);
6041 break;
6042 default:
6043 // do nothing. unrecognized key.
6044 break;
6045 }
6046 break;
6047 case 'inbox':
6048 switch(true) {
6049 case checkKeysForEvent(e, this.options.toggleHelp.value):
6050 this.toggleHelp();
6051 break;
6052 case checkKeysForEvent(e, this.options.toggleCmdLine.value):
6053 this.toggleCmdLine();
6054 break;
6055 case checkKeysForEvent(e, this.options.moveUp.value):
6056 this.moveUp();
6057 break;
6058 case checkKeysForEvent(e, this.options.moveDown.value):
6059 this.moveDown();
6060 break;
6061 case checkKeysForEvent(e, this.options.toggleChildren.value):
6062 this.toggleChildren();
6063 break;
6064 case checkKeysForEvent(e, this.options.upVote.value):
6065 this.upVote();
6066 break;
6067 case checkKeysForEvent(e, this.options.downVote.value):
6068 this.downVote();
6069 break;
6070 case checkKeysForEvent(e, this.options.reply.value):
6071 e.preventDefault();
6072 this.reply();
6073 break;
6074 case checkKeysForEvent(e, this.options.frontPage.value):
6075 e.preventDefault();
6076 this.frontPage();
6077 break;
6078 default:
6079 // do nothing. unrecognized key.
6080 break;
6081 }
6082 break;
6083 }
6084 } else {
6085 // console.log('ignored keypress');
6086 }
6087 },
6088 toggleHelp: function() {
6089 (document.getElementById('keyHelp').style.display === 'block') ? this.hideHelp() : this.showHelp();
6090 },
6091 showHelp: function() {
6092 // show help!
6093 RESUtils.fadeElementIn(document.getElementById('keyHelp'), 0.3);
6094 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'keyboardnavhelp');
6095 },
6096 hideHelp: function() {
6097 // hide help!
6098 RESUtils.fadeElementOut(document.getElementById('keyHelp'), 0.3);
6099 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'keyboardnavhelp');
6100 },
6101 hide: function() {
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) {
6107 this.moveDown();
6108 }
6109 },
6110 followSubreddit: function(newWindow) {
6111 // find the subreddit link and click it...
6112 var srLink = this.keyboardLinks[this.activeIndex].querySelector('A.subreddit');
6113 if (srLink) {
6114 var thisHREF = srLink.getAttribute('href');
6115 if (newWindow) {
6116 var button = (this.options.followLinkNewTabFocus.value) ? 0 : 1,
6117 thisJSON;
6118 if (BrowserDetect.isChrome()) {
6119 thisJSON = {
6120 requestType: 'keyboardNav',
6121 linkURL: thisHREF,
6122 button: button
6123 };
6124 chrome.extension.sendMessage(thisJSON);
6125 } else if (BrowserDetect.isSafari()) {
6126 thisJSON = {
6127 requestType: 'keyboardNav',
6128 linkURL: thisHREF,
6129 button: button
6130 };
6131 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
6132 } else if (BrowserDetect.isOpera()) {
6133 thisJSON = {
6134 requestType: 'keyboardNav',
6135 linkURL: thisHREF,
6136 button: button
6137 };
6138 opera.extension.postMessage(JSON.stringify(thisJSON));
6139 } else if (BrowserDetect.isFirefox()) {
6140 thisJSON = {
6141 requestType: 'keyboardNav',
6142 linkURL: thisHREF,
6143 button: button
6144 };
6145 self.postMessage(thisJSON);
6146 } else {
6147 window.open(thisHREF);
6148 }
6149 } else {
6150 location.href = thisHREF;
6151 }
6152 }
6153 },
6154 moveUp: function() {
6155 if (this.activeIndex > 0) {
6156 this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
6157 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)) {
6161 this.activeIndex--;
6162 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6163 }
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);
6167 }
6168
6169 modules['keyboardNav'].recentKey();
6170 }
6171 },
6172 moveDown: function() {
6173 if (this.activeIndex < this.keyboardLinks.length-1) {
6174 this.keyUnfocus(this.keyboardLinks[this.activeIndex]);
6175 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)) {
6179 this.activeIndex++;
6180 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6181 }
6182 this.keyFocus(this.keyboardLinks[this.activeIndex]);
6183 // console.log('xy: ' + RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]).toSource());
6184 /*
6185 if ((!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) || (this.options.scrollTop.value)) {
6186 RESUtils.scrollTo(0,thisXY.y);
6187 }
6188 */
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);
6195 } else {
6196 RESUtils.scrollTo(0,thisXY.y - window.innerHeight + thisHeight + 5);
6197 }
6198 }
6199 if ((RESUtils.pageType() === 'linklist') && (this.activeIndex == (this.keyboardLinks.length-1) && (modules['neverEndingReddit'].isEnabled() && modules['neverEndingReddit'].options.autoLoad.value))) {
6200 this.nextPage();
6201 }
6202 modules['keyboardNav'].recentKey();
6203 }
6204 },
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);
6212 }
6213 modules['keyboardNav'].recentKey();
6214 },
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);
6222 }
6223 modules['keyboardNav'].recentKey();
6224 },
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)) {
6234 this.activeIndex++;
6235 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6236 }
6237 if ((this.pageType === 'linklist') || (this.pageType === 'profile')) {
6238 this.setKeyIndex();
6239 }
6240 this.keyFocus(this.keyboardLinks[this.activeIndex]);
6241 if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
6242 RESUtils.scrollTo(0,thisXY.y);
6243 }
6244 }
6245 modules['keyboardNav'].recentKey();
6246 },
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,
6251 childCount;
6252 if (thisParent.previousSibling !== null) {
6253 childCount = thisParent.previousSibling.previousSibling.querySelectorAll('.entry').length;
6254 } else {
6255 childCount = 1;
6256 }
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)) {
6261 this.activeIndex++;
6262 thisXY=RESUtils.getXYpos(this.keyboardLinks[this.activeIndex]);
6263 }
6264 if ((this.pageType === 'linklist') || (this.pageType === 'profile')) {
6265 this.setKeyIndex();
6266 }
6267 this.keyFocus(this.keyboardLinks[this.activeIndex]);
6268 if (!(RESUtils.elementInViewport(this.keyboardLinks[this.activeIndex]))) {
6269 RESUtils.scrollTo(0,thisXY.y);
6270 }
6271 }
6272 modules['keyboardNav'].recentKey();
6273 },
6274 moveUpThread: function() {
6275 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
6276 this.moveToTopComment();
6277 }
6278 this.moveUpSibling();
6279 },
6280 moveDownThread: function() {
6281 if ((this.activeIndex < this.keyboardLinks.length-1) && (this.activeIndex > 1)) {
6282 this.moveToTopComment();
6283 }
6284 this.moveDownSibling();
6285 },
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;
6293 }
6294 }
6295 },
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);
6310 }
6311 }
6312 }
6313 }
6314 modules['keyboardNav'].recentKey();
6315 },
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, {});
6322 }
6323 }
6324 },
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.
6328 this.followLink();
6329 } else {
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');
6335 } else {
6336 // check if this is a "show more comments" box, or just contracted content...
6337 moreComments = thisNonCollapsed.querySelector('span.morecomments > a');
6338 if (moreComments) {
6339 thisToggle = moreComments;
6340 } else {
6341 thisToggle = thisNonCollapsed.querySelector('a.expand');
6342 }
6343 // 'continue this thread' links
6344 contThread = thisNonCollapsed.querySelector('span.deepthread > a');
6345 if(contThread){
6346 thisToggle = contThread;
6347 }
6348 }
6349 RESUtils.click(thisToggle);
6350 }
6351 },
6352 toggleExpando: function() {
6353 var thisExpando = this.keyboardLinks[this.activeIndex].querySelector('.expando-button');
6354 if (thisExpando) {
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);
6359 }
6360 }
6361 },
6362 imageResize: function(factor) {
6363 var images = $(this.activeElement).find('.RESImage.loaded'),
6364 thisWidth;
6365
6366 for (var i=0, len=images.length; i<len; i++) {
6367 thisWidth = images[i].width;
6368 modules['showImages'].resizeImage(images[i], thisWidth + factor);
6369 }
6370 },
6371 imageSizeUp: function(fineControl) {
6372 var factor = (fineControl) ? 50 : 150;
6373 this.imageResize(factor);
6374 },
6375 imageSizeDown: function(fineControl) {
6376 var factor = (fineControl) ? -50 : -150;
6377 this.imageResize(factor);
6378 },
6379 previousGalleryImage: function() {
6380 var previousButton = this.keyboardLinks[this.activeIndex].querySelector('.RESGalleryControls .previous');
6381 if (previousButton) {
6382 RESUtils.click(previousButton);
6383 }
6384 },
6385 nextGalleryImage: function() {
6386 var nextButton = this.keyboardLinks[this.activeIndex].querySelector('.RESGalleryControls .next');
6387 if (nextButton) {
6388 RESUtils.click(nextButton);
6389 }
6390 },
6391 toggleViewImages: function() {
6392 var thisViewImages = document.body.querySelector('#viewImagesButton');
6393 if (thisViewImages) {
6394 RESUtils.click(thisViewImages);
6395 }
6396 },
6397 toggleAllExpandos: function() {
6398 var thisExpandos = this.keyboardLinks[this.activeIndex].querySelectorAll('.expando-button');
6399 if (thisExpandos) {
6400 for (var i=0,len=thisExpandos.length; i<len; i++) {
6401 RESUtils.click(thisExpandos[i]);
6402 }
6403 }
6404 },
6405 followLink: function(newWindow) {
6406 var thisA = this.keyboardLinks[this.activeIndex].querySelector('a.title');
6407 var thisHREF = thisA.getAttribute('href');
6408 // console.log(thisA);
6409 if (newWindow) {
6410 var button = (this.options.followLinkNewTabFocus.value) ? 0 : 1,
6411 thisJSON;
6412 if (BrowserDetect.isChrome()) {
6413 thisJSON = {
6414 requestType: 'keyboardNav',
6415 linkURL: thisHREF,
6416 button: button
6417 };
6418 chrome.extension.sendMessage(thisJSON);
6419 } else if (BrowserDetect.isSafari()) {
6420 thisJSON = {
6421 requestType: 'keyboardNav',
6422 linkURL: thisHREF,
6423 button: button
6424 };
6425 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
6426 } else if (BrowserDetect.isOpera()) {
6427 thisJSON = {
6428 requestType: 'keyboardNav',
6429 linkURL: thisHREF,
6430 button: button
6431 };
6432 opera.extension.postMessage(JSON.stringify(thisJSON));
6433 } else if (BrowserDetect.isFirefox()) {
6434 thisJSON = {
6435 requestType: 'keyboardNav',
6436 linkURL: thisHREF,
6437 button: button
6438 };
6439 self.postMessage(thisJSON);
6440 } else {
6441 window.open(thisHREF);
6442 }
6443 } else {
6444 location.href = thisHREF;
6445 }
6446 },
6447 followComments: function(newWindow) {
6448 var thisA = this.keyboardLinks[this.activeIndex].querySelector('a.comments'),
6449 thisHREF = thisA.getAttribute('href');
6450 if (newWindow) {
6451 var thisJSON;
6452 if (BrowserDetect.isChrome()) {
6453 thisJSON = {
6454 requestType: 'keyboardNav',
6455 linkURL: thisHREF
6456 };
6457 chrome.extension.sendMessage(thisJSON);
6458 } else if (BrowserDetect.isSafari()) {
6459 thisJSON = {
6460 requestType: 'keyboardNav',
6461 linkURL: thisHREF
6462 };
6463 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
6464 } else if (BrowserDetect.isOpera()) {
6465 thisJSON = {
6466 requestType: 'keyboardNav',
6467 linkURL: thisHREF
6468 };
6469 opera.extension.postMessage(JSON.stringify(thisJSON));
6470 } else if (BrowserDetect.isFirefox()) {
6471 thisJSON = {
6472 requestType: 'keyboardNav',
6473 linkURL: thisHREF
6474 };
6475 self.postMessage(thisJSON);
6476 } else {
6477 window.open(thisHREF);
6478 }
6479 } else {
6480 location.href = thisHREF;
6481 }
6482 },
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);
6487 },
6488 upVote: function(link) {
6489 if (typeof this.keyboardLinks[this.activeIndex] === 'undefined') return false;
6490
6491 var upVoteButton;
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');
6494 } else {
6495 upVoteButton = this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.up') || this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.upmod');
6496 }
6497
6498 RESUtils.click(upVoteButton);
6499
6500 if (link && this.options.onVoteMoveDown.value) {
6501 this.moveDown();
6502 }
6503 },
6504 downVote: function(link) {
6505 if (typeof this.keyboardLinks[this.activeIndex] === 'undefined') return false;
6506
6507 var downVoteButton;
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');
6510 } else {
6511 downVoteButton = this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.down') || this.keyboardLinks[this.activeIndex].previousSibling.querySelector('div.downmod');
6512 }
6513
6514 RESUtils.click(downVoteButton);
6515
6516 if (link && this.options.onVoteMoveDown.value) {
6517 this.moveDown();
6518 }
6519 },
6520 saveLink: function() {
6521 var saveLink = this.keyboardLinks[this.activeIndex].querySelector('form.save-button > span > a');
6522 if (saveLink) RESUtils.click(saveLink);
6523 },
6524 saveComment: function() {
6525 var saveComment = this.keyboardLinks[this.activeIndex].querySelector('.saveComments');
6526 if (saveComment) RESUtils.click(saveComment);
6527 },
6528 reply: function() {
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();
6533 } else {
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]);
6538 }
6539 }
6540 }
6541 } else {
6542 infoBar = document.body.querySelector('.infobar');
6543 // We're on the original post, so shift keyboard focus to the comment reply box.
6544 if (infoBar) {
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');
6548 } else {
6549 var firstCommentBox = document.querySelector('.commentarea textarea[name=text]');
6550 firstCommentBox.focus();
6551 }
6552 }
6553 },
6554 navigateTo: function(newWindow,thisHREF) {
6555 if (newWindow) {
6556 var thisJSON;
6557 if (BrowserDetect.isChrome()) {
6558 thisJSON = {
6559 requestType: 'keyboardNav',
6560 linkURL: thisHREF
6561 };
6562 chrome.extension.sendMessage(thisJSON);
6563 } else if (BrowserDetect.isSafari()) {
6564 thisJSON = {
6565 requestType: 'keyboardNav',
6566 linkURL: thisHREF
6567 };
6568 safari.self.tab.dispatchMessage("keyboardNav", thisJSON);
6569 } else if (BrowserDetect.isOpera()) {
6570 thisJSON = {
6571 requestType: 'keyboardNav',
6572 linkURL: thisHREF
6573 };
6574 opera.extension.postMessage(JSON.stringify(thisJSON));
6575 } else {
6576 window.open(thisHREF);
6577 }
6578 } else {
6579 location.href = thisHREF;
6580 }
6581 },
6582 inbox: function(newWindow) {
6583 var thisHREF = location.protocol + '//'+location.hostname+'/message/inbox/';
6584 modules['keyboardNav'].navigateTo(newWindow,thisHREF);
6585 },
6586 profile: function(newWindow) {
6587 var thisHREF = location.protocol + '//'+location.hostname+'/user/'+RESUtils.loggedInUser();
6588 modules['keyboardNav'].navigateTo(newWindow,thisHREF);
6589 },
6590 frontPage: function(subreddit) {
6591 var newhref = location.protocol + '//'+location.hostname+'/';
6592 if (subreddit) {
6593 newhref += 'r/' + RESUtils.currentSubreddit();
6594 }
6595 location.href = newhref;
6596 },
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);
6601 this.moveBottom();
6602 } else {
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');
6609 }
6610 }
6611 },
6612 prevPage: function() {
6613 // if Never Ending Reddit is enabled, do nothing. Otherwise, click the 'prev' link.
6614 if (modules['neverEndingReddit'].isEnabled()) {
6615 return false;
6616 } else {
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');
6623 }
6624 }
6625 },
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:"])');
6629 },
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;
6637 }
6638 // RESUtils.click(thisLink);
6639 this.handleKeyLink(thisLink);
6640 }
6641 }
6642 }
6643 };
6644
6645 // user tagger functions
6646 modules['userTagger'] = {
6647 moduleID: 'userTagger',
6648 moduleName: 'User Tagger',
6649 category: 'Users',
6650 options: {
6651 /*
6652 defaultMark: {
6653 type: 'text',
6654 value: '_',
6655 description: 'clickable mark for users with no tag'
6656 },
6657 */
6658 hardIgnore: {
6659 type: 'boolean',
6660 value: false,
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).'
6662 },
6663 colorUser: {
6664 type: 'boolean',
6665 value: true,
6666 description: 'Color users based on cumulative upvotes / downvotes'
6667 },
6668 storeSourceLink: {
6669 type: 'boolean',
6670 value: true,
6671 description: 'By default, store a link to the link/comment you tagged a user on'
6672 },
6673 hoverInfo: {
6674 type: 'boolean',
6675 value: true,
6676 description: 'Show information on user (karma, how long they\'ve been a redditor) on hover.'
6677 },
6678 hoverDelay: {
6679 type: 'text',
6680 value: 800,
6681 description: 'Delay, in milliseconds, before hover tooltip loads. Default is 800.'
6682 },
6683 fadeDelay: {
6684 type: 'text',
6685 value: 200,
6686 description: 'Delay, in milliseconds, before hover tooltip fades away. Default is 200.'
6687 },
6688 fadeSpeed: {
6689 type: 'text',
6690 value: 0.3,
6691 description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
6692 },
6693 gildComments: {
6694 type: 'boolean',
6695 value: true,
6696 description: 'When clicking the "give gold" button on the user hover info on a comment, give gold to the comment.'
6697 },
6698 highlightButton: {
6699 type: 'boolean',
6700 value: true,
6701 description: 'Show "highlight" button in user hover info, for distinguishing posts/comments from particular users.'
6702 },
6703 highlightColor: {
6704 type: 'text',
6705 value: '#5544CC',
6706 description: 'Color used to highlight a selected user, when "highlighted" from hover info.'
6707 },
6708 highlightColorHover: {
6709 type: 'text',
6710 value: '#6677AA',
6711 description: 'Color used to highlight a selected user on hover.'
6712 },
6713 USDateFormat: {
6714 type: 'boolean',
6715 value: false,
6716 description: 'Show date (redditor since...) in US format (i.e. 08-31-2010)'
6717 },
6718 vwNumber: {
6719 type: 'boolean',
6720 value: true,
6721 description: 'Show the number (i.e. [+6]) rather than [vw]'
6722 },
6723 vwTooltip: {
6724 type: 'boolean',
6725 value: true,
6726 description: 'Show the vote weight tooltip on hover (i.e. "your votes for...")'
6727 }
6728 },
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);
6732 },
6733 isMatchURL: function() {
6734 return RESUtils.isMatchURL(this.moduleID);
6735 },
6736 include: [
6737 /^https?:\/\/([-\w\.]+\.)?reddit\.com\/[-\w\.]*/i
6738 ],
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; }';
6749
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; }';
6764
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=)}';
6778
6779 RESUtils.addCSS(css);
6780 }
6781 },
6782 go: function() {
6783 if ((this.isEnabled()) && (this.isMatchURL())) {
6784
6785 this.usernameRE = /(?:u|user)\/([\w\-]+)/;
6786 // Get user tag data...
6787 var tags = RESStorage.getItem('RESmodules.userTagger.tags');
6788 this.tags = null;
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();
6793 }
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();
6805 });
6806
6807 var tagsPerPage = parseInt(modules['dashboard'].options['tagsPerPage'].value, 10);
6808 if (tagsPerPage) {
6809 var controlWrapper = document.createElement('div');
6810 controlWrapper.id = 'tagPageControls';
6811 controlWrapper.className = 'RESGalleryControls';
6812 controlWrapper.page = 1;
6813 controlWrapper.pageCount = 1;
6814
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;
6820 } else {
6821 controlWrapper.page -= 1;
6822 }
6823 modules['userTagger'].drawUserTagTable();
6824 });
6825 controlWrapper.appendChild(leftButton);
6826
6827 var posLabel = document.createElement('span');
6828 posLabel.className = 'RESGalleryLabel';
6829 posLabel.textContent = "1 of 2";
6830 controlWrapper.appendChild(posLabel);
6831
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;
6837 } else {
6838 controlWrapper.page += 1;
6839 }
6840 modules['userTagger'].drawUserTagTable();
6841 });
6842 controlWrapper.appendChild(rightButton);
6843
6844 $('#userTaggerContents').append(controlWrapper);
6845
6846 }
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) {
6851 e.preventDefault();
6852 if ($(this).hasClass('delete')) {
6853 return false;
6854 }
6855 if ($(this).hasClass('active')) {
6856 $(this).toggleClass('descending');
6857 }
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'));
6863 });
6864 this.drawUserTagTable();
6865
6866 }
6867
6868
6869 // set up an array to cache user data
6870 this.authorInfoCache = [];
6871 if (this.options.colorUser.value) {
6872 this.attachVoteHandlers(document.body);
6873 }
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">&times;</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>';
6884 }
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) {
6903 e.preventDefault();
6904 modules['userTagger'].saveTagForm();
6905 }, false);
6906 var userTaggerClose = this.userTaggerToolTip.querySelector('#userTaggerClose');
6907 userTaggerClose.addEventListener('click', function(e) {
6908 modules['userTagger'].closeUserTagPrompt();
6909 }, false);
6910 //this.userTaggerToolTip.appendChild(userTaggerSave);
6911 this.userTaggerForm = this.userTaggerToolTip.querySelector('FORM');
6912 this.userTaggerForm.addEventListener('submit', function(e) {
6913 e.preventDefault();
6914 modules['userTagger'].saveTagForm();
6915 }, true);
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);
6927 }
6928 modules['userTagger'].hideAuthorInfo();
6929 }, false);
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);
6935 }
6936 }, false);
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);
6942 }
6943 }, false);
6944 document.body.appendChild(this.authorInfoToolTip);
6945 }
6946 document.getElementById('userTaggerTag').addEventListener('keydown', function(e) {
6947 if (e.keyCode === 27) {
6948 // close prompt.
6949 modules['userTagger'].closeUserTagPrompt();
6950 }
6951 }, true);
6952 //console.log('before applytags: ' + Date());
6953 this.applyTags();
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);
6958 } else {
6959 RESUtils.watchForElement('siteTable', modules['userTagger'].attachVoteHandlers);
6960 RESUtils.watchForElement('siteTable', modules['userTagger'].applyTags);
6961 }
6962
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) : '';
6971 } else {
6972 var thisFriendComment = '';
6973 }
6974 // this stopped working. commenting it out for now. if i add this back I need to check if you're reddit gold anyway.
6975 /*
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 );
6980 */
6981 }
6982 }
6983 }
6984 },
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;
6994 }
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;
6999 }
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);
7004 }
7005 }
7006 },
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');
7017 }
7018 if (thisAuthorA) {
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;
7023 var votes = 0;
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);
7027 }
7028 } else {
7029 modules['userTagger'].tags[thisAuthor] = {};
7030 }
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?
7040 switch (upOrDown) {
7041 case 'up':
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.
7047 votes--;
7048 modules['userTagger'].voteStates[pairNum] = 0;
7049 } else {
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...
7052 votes++;
7053 // if it was previously downvoted, add another!
7054 if (modules['userTagger'].voteStates[pairNum] === -1) {
7055 votes++;
7056 }
7057 modules['userTagger'].voteStates[pairNum] = 1;
7058 }
7059 break;
7060 case 'down':
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.
7066 votes++;
7067 modules['userTagger'].voteStates[pairNum] = 0;
7068 } else {
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...
7071 votes--;
7072 // if it was previously upvoted, subtract another!
7073 if (modules['userTagger'].voteStates[pairNum] === 1) {
7074 votes--;
7075 }
7076 modules['userTagger'].voteStates[pairNum] = -1;
7077 }
7078 break;
7079 }
7080 /*
7081 if ((hasClass(this, 'upmod')) || (hasClass(this, 'down'))) {
7082 // upmod = upvote. down = undo of downvote.
7083 votes = votes + 1;
7084 } else if ((hasClass(this, 'downmod')) || (hasClass(this, 'up'))) {
7085 // downmod = downvote. up = undo of downvote.
7086 votes = votes - 1;
7087 }
7088 */
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);
7092 }
7093 }
7094 },
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);
7103 } else {
7104 taggedUsers.push(i);
7105 }
7106 }
7107 switch (this.currentSortMethod) {
7108 case 'tag':
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;
7113 });
7114 if (this.descending) taggedUsers.reverse();
7115 break;
7116 case 'ignore':
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;
7121 });
7122 if (this.descending) taggedUsers.reverse();
7123 break;
7124 case 'color':
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;
7129 });
7130 if (this.descending) taggedUsers.reverse();
7131 break;
7132 case 'votes':
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());
7137 });
7138 if (this.descending) taggedUsers.reverse();
7139 break;
7140 default:
7141 // sort users, ignoring case
7142 taggedUsers.sort(function(a,b) {
7143 return (a.toLowerCase() > b.toLowerCase()) ? 1 : (b.toLowerCase() > a.toLowerCase()) ? -1 : 0;
7144 });
7145 if (this.descending) taggedUsers.reverse();
7146 break;
7147 }
7148 $('#userTaggerTable tbody').html('');
7149 var tagsPerPage = parseInt(modules['dashboard'].options['tagsPerPage'].value, 10);
7150 var count = taggedUsers.length;
7151 var start = 0;
7152 var end = count;
7153
7154 if (tagsPerPage) {
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);
7164 }
7165
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';
7172
7173 var userTagLink = document.createElement('a');
7174 if (thisTag === '') {
7175 // thisTag = '<div class="RESUserTagImage"></div>';
7176 userTagLink.setAttribute('class','userTagLink RESUserTagImage');
7177 } else {
7178 userTagLink.setAttribute('class','userTagLink hasTag');
7179 }
7180 $(userTagLink).html(escapeHTML(thisTag));
7181 if (thisColor) {
7182 var bgColor = (thisColor === 'none') ? 'transparent' : thisColor;
7183 userTagLink.setAttribute('style','background-color: '+bgColor+'; color: '+this.bgToTextColorMap[thisColor]+' !important;');
7184 }
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'));
7190 }, true);
7191
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);
7194 }
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+"?");
7198 if (answer) {
7199 delete modules['userTagger'].tags[thisUser];
7200 RESStorage.setItem('RESmodules.userTagger.tags', JSON.stringify(modules['userTagger'].tags));
7201 $(this).closest('tr').remove();
7202 }
7203 });
7204 },
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);
7214 },
7215 bgToTextColorMap: {
7216 'none': 'black',
7217 'aqua': 'black',
7218 'black': 'white',
7219 'blue': 'white',
7220 'fuchsia': 'white',
7221 'pink': 'black',
7222 'gray': 'white',
7223 'green': 'white',
7224 'lime': 'black',
7225 'maroon': 'white',
7226 'navy': 'white',
7227 'olive': 'black',
7228 'orange': 'black',
7229 'purple': 'white',
7230 'red':' black',
7231 'silver': 'black',
7232 'teal': 'white',
7233 'white': 'black',
7234 'yellow': 'black'
7235 },
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;
7242 var thisTag = null;
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;
7247 } else {
7248 document.getElementById('userTaggerLink').value = '';
7249 }
7250 if (typeof this.tags[username].tag !== 'undefined') {
7251 document.getElementById('userTaggerTag').value = this.tags[username].tag;
7252 } else {
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);
7258 }
7259 }
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');
7264 } else {
7265 document.getElementById('userTaggerIgnore').checked = false;
7266 }
7267 if (typeof this.tags[username].votes !== 'undefined') {
7268 document.getElementById('userTaggerVoteWeight').value = this.tags[username].votes;
7269 } else {
7270 document.getElementById('userTaggerVoteWeight').value = '';
7271 }
7272 if (typeof this.tags[username].color !== 'undefined') {
7273 RESUtils.setSelectValue(document.getElementById('userTaggerColor'), this.tags[username].color);
7274 } else {
7275 document.getElementById('userTaggerColor').selectedIndex = 0;
7276 }
7277 } else {
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);
7284 }
7285 document.getElementById('userTaggerColor').selectedIndex = 0;
7286 }
7287 this.userTaggerToolTip.setAttribute('style', 'display: block; top: ' + thisXY.y + 'px; left: ' + thisXY.x + 'px;');
7288 document.getElementById('userTaggerTag').focus();
7289 modules['userTagger'].updateTagPreview();
7290 return false;
7291 },
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');
7298 }
7299 if (linkTitle.length) {
7300 document.getElementById('userTaggerLink').value = $(linkTitle).attr('href');
7301 } else {
7302 var permaLink = $(closestEntry).find('.flat-list.buttons li.first a');
7303 if (permaLink.length) {
7304 document.getElementById('userTaggerLink').value = $(permaLink).attr('href');
7305 }
7306 }
7307 },
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];
7313 },
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++) {
7320 inputs[i].blur();
7321 }
7322 }
7323 },
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;
7332 if (ignore) {
7333 this.tags[username].ignore = true;
7334 } else {
7335 delete this.tags[username].ignore;
7336 }
7337 if (!noclick) {
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));
7341 }
7342 } else {
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;
7349 }
7350 if (!noclick) {
7351 this.clickedTag.setAttribute('style', 'background-color: transparent');
7352 this.clickedTag.setAttribute('class', 'userTagLink RESUserTagImage');
7353 $(this.clickedTag).html('');
7354 }
7355 }
7356
7357 if (typeof this.tags[username] !== 'undefined') {
7358 this.tags[username].votes = (isNaN(votes)) ? 0 : votes;
7359 }
7360 if (!noclick) {
7361 var thisVW = this.clickedTag.parentNode.parentNode.querySelector('a.voteWeight');
7362 if (thisVW) {
7363 this.colorUser(thisVW, username, votes);
7364 }
7365 }
7366 if (RESUtils.isEmpty(this.tags[username])) delete this.tags[username];
7367 RESStorage.setItem('RESmodules.userTagger.tags', JSON.stringify(this.tags));
7368 this.closeUserTagPrompt();
7369 },
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);
7375 });
7376 },
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);
7387 }, false);
7388 thisAuthorObj.addEventListener('mouseout', function(e) {
7389 clearTimeout(modules['userTagger'].showTimer);
7390 }, false);
7391 thisAuthorObj.addEventListener('click', function(e) {
7392 clearTimeout(modules['userTagger'].showTimer);
7393 }, false);
7394 }
7395 var test = thisAuthorObj.href.match(this.usernameRE);
7396 if (test) var thisAuthor = test[1];
7397 // var thisAuthor = thisAuthorObj.text;
7398 var noTag = false;
7399 if ((thisAuthor) && (thisAuthor.substr(0,3) === '/u/')) {
7400 noTag = true;
7401 thisAuthor = thisAuthor.substr(3);
7402 }
7403 if (!noTag) {
7404 thisAuthorObj.classList.add('userTagged');
7405 if (typeof userObject[thisAuthor] === 'undefined') {
7406 var thisVotes = 0;
7407 var thisTag = null;
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);
7413 }
7414 if (typeof this.tags[thisAuthor].tag !== 'undefined') {
7415 thisTag = this.tags[thisAuthor].tag;
7416 }
7417 if (typeof this.tags[thisAuthor].color !== 'undefined') {
7418 thisColor = this.tags[thisAuthor].color;
7419 }
7420 if (typeof this.tags[thisAuthor].ignore !== 'undefined') {
7421 thisIgnore = this.tags[thisAuthor].ignore;
7422 }
7423 }
7424 userObject[thisAuthor] = {
7425 tag: thisTag,
7426 color: thisColor,
7427 ignore: thisIgnore,
7428 votes: thisVotes
7429 }
7430 }
7431
7432 var userTagFrag = document.createDocumentFragment();
7433
7434 var userTagLink = document.createElement('a');
7435 if (!(thisTag)) {
7436 // thisTag = '<div class="RESUserTagImage"></div>';
7437 userTagLink.setAttribute('class','userTagLink RESUserTagImage');
7438 } else {
7439 userTagLink.setAttribute('class','userTagLink hasTag');
7440 }
7441 $(userTagLink).html(escapeHTML(thisTag));
7442 if (thisColor) {
7443 var bgColor = (thisColor === 'none') ? 'transparent' : thisColor;
7444 userTagLink.setAttribute('style','background-color: '+bgColor+'; color: '+this.bgToTextColorMap[thisColor]+' !important;');
7445 }
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'));
7451 }, true);
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'));
7471 }, true);
7472 this.colorUser(userVoteWeight, thisAuthor, userObject[thisAuthor].votes);
7473 userVoteFrag.appendChild(userVoteWeight);
7474 userTagFrag.appendChild(userVoteFrag);
7475 }
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');
7482 if (thisComment) {
7483 $(thisComment).textContent = thisAuthor + ' is an ignored user';
7484 thisComment.classList.add('ignoredUserComment');
7485
7486 var toggle = thisComment.parentNode.querySelector('a.expand');
7487 RESUtils.click(toggle);
7488 }
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();
7492 } else {
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';
7499 }
7500 }
7501 } else {
7502 if (RESUtils.pageType() === 'comments') {
7503 var thisComment = thisAuthorObj.parentNode.parentNode.querySelector('.usertext');
7504 if (thisComment) {
7505 thisComment.textContent = thisAuthor + ' is an ignored user';
7506 thisComment.classList.add('ignoredUserComment');
7507 }
7508 } else {
7509 var thisPost = thisAuthorObj.parentNode.parentNode.parentNode.querySelector('p.title');
7510 if (thisPost) {
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';
7514 }, 100);
7515 thisPost.setAttribute('class','ignoredUserPost');
7516 }
7517 }
7518 }
7519 }
7520 }
7521 }
7522 },
7523 colorUser: function(obj, author, votes) {
7524 if (this.options.colorUser.value) {
7525 votes = parseInt(votes, 10);
7526 var red = 255;
7527 var green = 255;
7528 var blue = 255;
7529 var voteString = '+';
7530 if (votes > 0) {
7531 red = Math.max(0, (255-(8*votes)));
7532 green = 255;
7533 blue = Math.max(0, (255-(8*votes)));
7534 } else if (votes < 0) {
7535 red = 255;
7536 green = Math.max(0, (255-Math.abs(8*votes)));
7537 blue = Math.max(0, (255-Math.abs(8*votes)));
7538 voteString = '';
7539 }
7540 voteString = voteString + votes;
7541 var rgb='rgb('+red+','+green+','+blue+')';
7542 if (obj !== null) {
7543 if (votes === 0) {
7544 obj.style.display = 'none';
7545 } else {
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));
7550 }
7551 }
7552 }
7553 },
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;
7565 if (isFriend) {
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>';
7567 } else {
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>';
7569 }
7570 var friendButtonEle = $(friendButton);
7571 $(modules['userTagger'].authorInfoToolTipHeader).append(friendButtonEle);
7572 });
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;');
7579 } else {
7580 this.authorInfoToolTip.classList.remove('right');
7581 this.authorInfoToolTip.setAttribute('style', 'top: ' + (thisXY.y - 14) + 'px; left: ' + (thisXY.x + thisWidth + 25) + 'px;');
7582 }
7583 if(this.options.fadeSpeed.value < 0 || this.options.fadeSpeed.value > 1 || isNaN(this.options.fadeSpeed.value)) {
7584 this.options.fadeSpeed.value = 0.3;
7585 }
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();
7591 }
7592 }, 1000);
7593 obj.addEventListener('mouseout', modules['userTagger'].delayedHideAuthorInfo);
7594 if (typeof this.authorInfoCache[thisUserName] !== 'undefined') {
7595 this.writeAuthorInfo(this.authorInfoCache[thisUserName], obj);
7596 } else {
7597 GM_xmlhttpRequest({
7598 method: "GET",
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);
7604 }
7605 });
7606 }
7607 },
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);
7613 },
7614 writeAuthorInfo: function(jsonData, authorLink) {
7615 if (!jsonData.data) {
7616 $(this.authorInfoToolTipContents).text("User not found");
7617 return false;
7618 }
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>';
7627 }
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>';
7632 } else {
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>';
7634 }
7635
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>';
7639 } else {
7640 userHTML += '<div class="redButton" id="highlightUser" user="'+escapeHTML(jsonData.data.name)+'">Unhighlight</div>';
7641 }
7642 }
7643
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)+'">&empty; Unignore</div>';
7646 } else {
7647 userHTML += '<div class="blueButton" id="ignoreUser" user="'+escapeHTML(jsonData.data.name)+'">&empty; Ignore</div>';
7648 }
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);
7659 }, false);
7660 }
7661 }
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;
7666
7667 var comment = $(authorLink).closest('.comment');
7668 if (!comment) return;
7669
7670 modules['userTagger'].hideAuthorInfo();
7671 var giveGold = comment.find('.give-gold')[0];
7672 RESUtils.click(giveGold);
7673 e.preventDefault();
7674 });
7675 }
7676 },
7677 toggleUserHighlight: function(username) {
7678 if (!this.highlightedUsers) this.highlightedUsers = {};
7679
7680 if (this.highlightedUsers[username]) {
7681 this.highlightedUsers[username].remove();
7682 delete this.highlightedUsers[username];
7683 this.toggleUserHighlightButton(true);
7684 } else {
7685
7686 this.highlightedUsers[username] =
7687 modules['userHighlight'].highlightUser(username);
7688 this.toggleUserHighlightButton(false);
7689 }
7690 },
7691 toggleUserHighlightButton: function(canHighlight) {
7692 $(this.authorInfoToolTipHighlight)
7693 .toggleClass('blueButton', canHighlight)
7694 .toggleClass('redButton', !canHighlight)
7695 .text(canHighlight ? 'Highlight' : 'Unhighlight');
7696 },
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('&empty; Unignore');
7702 var thisIgnore = true;
7703 } else {
7704 e.target.classList.remove('redButton');
7705 e.target.classList.add('blueButton');
7706 $(e.target).html('&empty; Ignore');
7707 var thisIgnore = false;
7708 }
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 || '';
7716 }
7717 if ((thisIgnore) && (thisTag === '')) {
7718 thisTag = 'ignored';
7719 } else if ((!thisIgnore) && (thisTag === 'ignored')) {
7720 thisTag = '';
7721 }
7722 modules['userTagger'].setUserTag(thisName, thisTag, thisColor, thisIgnore, thisLink, thisVotes, true); // last true is for noclick param
7723 },
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;
7728 }
7729 RESUtils.fadeElementOut(this.authorInfoToolTip, this.options.fadeSpeed.value);
7730 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'authorInfo');
7731 },
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;
7736 var tags = {};
7737 var toRemove = [];
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('.');
7741 if (keySplit) {
7742 var keyRoot = keySplit[0];
7743 switch (keyRoot) {
7744 case 'reddituser':
7745 var thisNode = keySplit[1];
7746 if (typeof tags[keySplit[2]] === 'undefined') {
7747 tags[keySplit[2]] = {};
7748 }
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));
7757 }
7758 // now delete the old stored garbage...
7759 var keyString = 'reddituser.'+thisNode+'.'+keySplit[2];
7760 toRemove.push(keyString);
7761 break;
7762 default:
7763 // console.log('Not currently handling keys with root: ' + keyRoot);
7764 break;
7765 }
7766 }
7767 }
7768 this.tags = tags;
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]);
7773 }
7774 }
7775 };
7776
7777 // betteReddit
7778 modules['betteReddit'] = {
7779 moduleID: 'betteReddit',
7780 moduleName: 'betteReddit',
7781 category: 'UI',
7782 options: {
7783 fullCommentsLink: {
7784 type: 'boolean',
7785 value: true,
7786 description: 'add "full comments" link to comment replies, etc'
7787 },
7788 fullCommentsText: {
7789 type: 'text',
7790 value: 'full comments',
7791 description: 'text of full comments link'
7792 },
7793 commentsLinksNewTabs: {
7794 type: 'boolean',
7795 value: false,
7796 description: 'Open links found in comments in a new tab'
7797 },
7798 fixSaveLinks: {
7799 type: 'boolean',
7800 value: true,
7801 description: 'Make "save" links change to "unsave" links when clicked'
7802 },
7803 fixHideLinks: {
7804 type: 'boolean',
7805 value: true,
7806 description: 'Make "hide" links change to "unhide" links when clicked, and provide a 5 second delay prior to hiding the link'
7807 },
7808 searchSubredditByDefault: {
7809 type: 'boolean',
7810 value: true,
7811 description: 'Search the current subreddit by default when using the search box, instead of all of reddit.'
7812 },
7813 showUnreadCount: {
7814 type: 'boolean',
7815 value: true,
7816 description: 'Show unread message count next to orangered?'
7817 },
7818 showUnreadCountInTitle: {
7819 type: 'boolean',
7820 value: false,
7821 description: 'Show unread message count in page/tab title?'
7822 },
7823 showUnreadCountInFavicon: {
7824 type: 'boolean',
7825 value: false,
7826 description: 'Show unread message count in favicon?'
7827 },
7828 unreadLinksToInbox: {
7829 type: 'boolean',
7830 value: false,
7831 description: 'Always go to the inbox, not unread messages, when clicking on orangered'
7832 },
7833 videoTimes: {
7834 type: 'boolean',
7835 value: true,
7836 description: 'Show lengths of videos when possible'
7837 },
7838 videoUploaded: {
7839 type: 'boolean',
7840 value: false,
7841 description: 'Show upload date of videos when possible'
7842 },
7843 toolbarFix: {
7844 type: 'boolean',
7845 value: true,
7846 description: 'Don\'t use Reddit Toolbar when linking to sites that may not function (twitter, youtube and others)'
7847 },
7848 pinHeader: {
7849 type: 'enum',
7850 values: [
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' }
7856 ],
7857 value: 'none',
7858 description: 'Pin the subreddit bar or header to the top, even when you scroll.'
7859 },
7860 turboSelfText: {
7861 type: 'boolean',
7862 value: true,
7863 description: 'Preload selftext data to make selftext expandos faster (preloads after first expando)'
7864 },
7865 showLastEditedTimestamp: {
7866 type: 'boolean',
7867 value: true,
7868 description: 'Show the time that a text post/comment was edited, without having to hover the timestamp.'
7869 },
7870 restoreSavedTab: {
7871 type: 'boolean',
7872 value: false,
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.'
7874 }
7875 },
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);
7879 },
7880 isMatchURL: function() {
7881 return RESUtils.isMatchURL(this.moduleID);
7882 },
7883 include: [
7884 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
7885 ],
7886 go: function() {
7887 if ((this.isEnabled()) && (this.isMatchURL())) {
7888
7889 if ((this.options.toolbarFix.value) && ((RESUtils.pageType() === 'linklist') || RESUtils.pageType() === 'comments')) {
7890 this.toolbarFix();
7891 }
7892 if ((RESUtils.pageType() === 'comments') && (this.options.commentsLinksNewTabs.value)) {
7893 this.commentsLinksNewTabs();
7894 }
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();
7900 }
7901 if ((RESUtils.pageType() === 'profile') && (location.href.split('/').indexOf(RESUtils.loggedInUser()) !== -1)) {
7902 this.editMyComments();
7903 }
7904 if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (this.options.fixSaveLinks.value)) {
7905 this.fixSaveLinks();
7906 }
7907 if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (this.options.fixHideLinks.value)) {
7908 this.fixHideLinks();
7909 }
7910 if ((this.options.turboSelfText.value) && (RESUtils.pageType() === 'linklist')) {
7911 this.setUpTurboSelfText();
7912 }
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);
7917 }
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%;}');
7920 }
7921 if ((modules['betteReddit'].options.restoreSavedTab.value) && (RESUtils.loggedInUser() !== null) && document.querySelector('.with-listing-chooser:not(.profile-page)')) {
7922 this.restoreSavedTab();
7923 }
7924 if ((modules['betteReddit'].options.toolbarFix.value) && (RESUtils.pageType() === 'linklist')) {
7925 RESUtils.watchForElement('siteTable', modules['betteReddit'].toolbarFix);
7926 }
7927 if ((RESUtils.pageType() === 'inbox') && (modules['betteReddit'].options.fullCommentsLink.value)) {
7928 RESUtils.watchForElement('siteTable', modules['betteReddit'].fullComments);
7929 }
7930 if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (modules['betteReddit'].options.fixSaveLinks.value)) {
7931 RESUtils.watchForElement('siteTable', modules['betteReddit'].fixSaveLinks);
7932 }
7933 if (((RESUtils.pageType() === 'linklist') || (RESUtils.pageType() === 'comments')) && (modules['betteReddit'].options.fixHideLinks.value)) {
7934 RESUtils.watchForElement('siteTable', modules['betteReddit'].fixHideLinks);
7935 }
7936 if ((RESUtils.pageType() === 'comments') && (modules['betteReddit'].options.commentsLinksNewTabs.value)) {
7937 RESUtils.watchForElement('newComments', modules['betteReddit'].commentsLinksNewTabs);
7938 }
7939
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();
7944 }
7945 }
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...
7949
7950 RESUtils.watchForElement('siteTable', modules['betteReddit'].getVideoTimes);
7951 }
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; }');
7962 }
7963 this.showUnreadCount();
7964 }
7965 switch(this.options.pinHeader.value) {
7966 case 'header':
7967 this.pinHeader();
7968 $('body').addClass('pinHeader-header');
7969 break;
7970 case 'sub':
7971 this.pinSubredditBar();
7972 $('body').addClass('pinHeader-sub');
7973 break;
7974 case 'subanduser':
7975 this.pinSubredditBar();
7976 this.pinUserBar();
7977 $('body').addClass('pinHeader-subanduser');
7978 break;
7979 case 'userbar':
7980 this.pinUserBar();
7981 $('body').addClass('pinHeader-userbar');
7982 break;
7983 default:
7984 break;
7985 }
7986 }
7987 },
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';
7993 }
7994 },
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');
8000
8001 RESUtils.watchForElement('siteTable', modules['betteReddit'].setNextSelftextURL);
8002 },
8003 setNextSelftextURL: function(ele) {
8004 if (modules['neverEndingReddit'].nextPageURL) {
8005 var jsonURL = modules['neverEndingReddit'].nextPageURL.replace('/?','/.json?');
8006 $(ele).data('jsonURL',jsonURL);
8007 }
8008 },
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);
8015 } else {
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() +
8024 '</div></form>'
8025 ).show();
8026 } else {
8027 $(event.target).removeClass('expanded');
8028 $(event.target).addClass('collapsedExpando');
8029 $(event.target).addClass('collapsed');
8030 $(event.target).parent().find('.expando').hide();
8031 }
8032
8033 }
8034 },
8035 getSelfTextData: function(href) {
8036 if (!modules['betteReddit'].gettingSelfTextData) {
8037 modules['betteReddit'].gettingSelfTextData = true;
8038 $.getJSON(href, modules['betteReddit'].applyTurboSelfText);
8039 }
8040 },
8041 applyTurboSelfText: function(data) {
8042 var linkList = data.data.children;
8043
8044 delete modules['betteReddit'].gettingSelfTextData;
8045 for (var i=0, len=linkList.length; i<len; i++) {
8046 var thisID = linkList[i].data.name;
8047 if (i === 0) {
8048 var thisSiteTable = $('.id-'+thisID).closest('.sitetable.linklisting');
8049 $(thisSiteTable).find('.expando-button.selftext').removeAttr('onclick');
8050 }
8051 modules['betteReddit'].selfTextHash[thisID] = linkList[i].data.selftext_html;
8052 }
8053 },
8054 getInboxLink: function (havemail) {
8055 if (havemail && !modules['betteReddit'].options.unreadLinksToInbox.value) {
8056 return '/message/unread/';
8057 }
8058
8059 return '/message/inbox/';
8060 },
8061 showUnreadCount: function() {
8062 if (typeof this.mail === 'undefined') {
8063 this.mail = document.querySelector('#mail');
8064 if (this.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);
8069 }
8070 }
8071 if (this.mail) {
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) {
8079 GM_xmlhttpRequest({
8080 method: "GET",
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);
8090 }
8091 });
8092 } else {
8093 var count = RESStorage.getItem('RESmodules.betteReddit.msgCount.'+RESUtils.loggedInUser());
8094 modules['betteReddit'].setUnreadCount(count);
8095 }
8096 } else {
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);
8100 }
8101 }
8102 },
8103 setUnreadCount: function(count) {
8104 if (this.options.showUnreadCountInFavicon.value) {
8105 //window.Tinycon.setOptions({ fallback: false });
8106 }
8107 if (count>0) {
8108 if (this.options.showUnreadCountInTitle.value) {
8109 var newTitle = '[' + count + '] ' + document.title.replace(/^\[[\d]+\]\s/,'');
8110 document.title = newTitle;
8111 }
8112 if (this.options.showUnreadCountInFavicon.value) {
8113 //window.Tinycon.setBubble(count);
8114 }
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+']';
8121 }
8122 }
8123 } else {
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('');
8132 }
8133 }
8134 if (this.options.showUnreadCountInFavicon.value) {
8135 //window.Tinycon.setBubble(0);
8136 }
8137 }
8138 },
8139 toolbarFixLinks: [
8140 'etsy.com',
8141 'youtube.com',
8142 'youtu.be',
8143 'twitter.com',
8144 'teamliquid.net',
8145 'flickr.com',
8146 'github.com',
8147 'battle.net',
8148 'play.google.com',
8149 'plus.google.com',
8150 'soundcloud.com'
8151 ],
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;
8155 }
8156 return false;
8157 },
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');
8164 }
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');
8169 }
8170 }
8171 }
8172 },
8173 fullComments: function(ele) {
8174 var root = ele || document;
8175 var entries = root.querySelectorAll('#siteTable .entry');
8176
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');
8182 }
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);
8191 }
8192 }
8193 },
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');
8202 }
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);
8208 }
8209 }
8210 },
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);
8218 }
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);
8225 }
8226 }
8227 },
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);
8235 }
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);
8242 }
8243 }
8244 },
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...');
8249 } else {
8250 $(modules['betteReddit'].saveLinkClicked).text('saving...');
8251 }
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';
8261 } else {
8262 var executed = 'saved';
8263 var apiURL = location.protocol + '//'+location.hostname+'/api/save';
8264 }
8265 var params = 'id='+linkid+'&executed='+executed+'&uh='+modules['betteReddit'].modhash+'&renderstyle=html';
8266 GM_xmlhttpRequest({
8267 method: "POST",
8268 url: apiURL,
8269 data: params,
8270 headers: {
8271 "Content-Type": "application/x-www-form-urlencoded"
8272 },
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');
8278 } else {
8279 $(modules['betteReddit'].saveLinkClicked).text('unsave');
8280 modules['betteReddit'].saveLinkClicked.setAttribute('action','unsave');
8281 }
8282 } else {
8283 delete modules['betteReddit'].modhash;
8284 alert('Sorry, there was an error trying to '+modules['betteReddit'].saveLinkClicked.getAttribute('action')+' your submission. Try clicking again.');
8285 }
8286 }
8287 });
8288 } else {
8289 GM_xmlhttpRequest({
8290 method: "GET",
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();
8299 }
8300 }
8301 });
8302 }
8303 },
8304 hideLinkEventHandler: function(e) {
8305 modules['betteReddit'].hideLink(e.target);
8306 },
8307 hideLink: function(clickedLink) {
8308 if (clickedLink.getAttribute('action') === 'unhide') {
8309 $(clickedLink).text('unhiding...');
8310 } else {
8311 $(clickedLink).text('hiding...');
8312 }
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';
8322 } else {
8323 var executed = 'hidden';
8324 var apiURL = 'http://'+location.hostname+'/api/hide';
8325 }
8326 var params = 'id='+linkid+'&executed='+executed+'&uh='+modules['betteReddit'].modhash+'&renderstyle=html';
8327
8328 GM_xmlhttpRequest({
8329 method: "POST",
8330 url: apiURL,
8331 data: params,
8332 headers: {
8333 "Content-Type": "application/x-www-form-urlencoded"
8334 },
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);
8341 } else {
8342 $(clickedLink).text('unhide');
8343 clickedLink.setAttribute('action','unhide');
8344 modules['betteReddit'].hideTimer = setTimeout(function() {
8345 modules['betteReddit'].hideFader(clickedLink);
8346 }, 5000);
8347 }
8348 } else {
8349 delete modules['betteReddit'].modhash;
8350 alert('Sorry, there was an error trying to '+clickedLink.getAttribute('action')+' your submission. Try clicking again.');
8351 }
8352 }
8353 });
8354 } else {
8355 GM_xmlhttpRequest({
8356 method: "GET",
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);
8365 }
8366 }
8367 });
8368 }
8369 },
8370 hideFader: function(ele) {
8371 var parentThing = ele.parentNode.parentNode.parentNode.parentNode.parentNode.parentNode;
8372 RESUtils.fadeElementOut(parentThing, 0.3);
8373 },
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;
8379 }
8380 },
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]*[\]|\)]/;
8386 if (youtubeLinks) {
8387 var ytLinks = [];
8388 for (var i=0, len=youtubeLinks.length; i<len; i+=1) {
8389 if(!youtubeLinks[i].innerHTML.match(titleHasTimeRegex)) {
8390 ytLinks.push(youtubeLinks[i]);
8391 }
8392 }
8393 youtubeLinks = ytLinks;
8394 var getYoutubeIDRegex = /\/?[\&|\?]?v\/?=?([\w\-]{11})&?/i;
8395 var getShortenedYoutubeIDRegex = /([\w\-]{11})&?/i;
8396 var getYoutubeStartTimeRegex = /\[[\d]+:[\d]+\]/i;
8397 var tempIDs = [];
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'));
8404 if (isShortened) {
8405 var smatch = getShortenedYoutubeIDRegex.exec(youtubeLinks[i].getAttribute('href'));
8406 if (smatch) {
8407 var thisYTID = '"' + smatch[1] + '"';
8408 modules['betteReddit'].youtubeLinkIDs[thisYTID] = youtubeLinks[i];
8409 modules['betteReddit'].youtubeLinkRefs.push([thisYTID, youtubeLinks[i]]);
8410 }
8411 } else if (match) {
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]]);
8416 }
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] + ')';
8421 }
8422 }
8423 for (var id in modules['betteReddit'].youtubeLinkIDs){
8424 tempIDs.push(id);
8425 }
8426 modules['betteReddit'].youtubeLinkIDs = tempIDs;
8427 modules['betteReddit'].getVideoJSON();
8428 }
8429 },
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';
8436 GM_xmlhttpRequest({
8437 method: "GET",
8438 url: jsonURL,
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 +']';
8454 }
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);
8459 }
8460 }
8461 }
8462 // wait a bit, make another request...
8463 setTimeout(modules['betteReddit'].getVideoJSON, 500);
8464 }
8465 }
8466 });
8467 }
8468 },
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();
8473
8474 var sb = document.getElementById('sr-header-area');
8475 if (sb == null) return; // reddit is under heavy load
8476 var header = document.getElementById('header');
8477
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;
8483
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;
8488
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);
8493
8494 // make it fixed
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);
8501 },
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();
8511 },
8512 handleScroll: function(e) {
8513 if (modules['betteReddit'].scrollTimer) clearTimeout(modules['betteReddit'].scrollTimer);
8514 modules['betteReddit'].scrollTimer = setTimeout(modules['betteReddit'].handleScrollAfterTimer, 300);
8515 },
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;');
8521 }
8522 } else if (modules['betteReddit'].options.pinHeader.value === 'subanduser') {
8523 if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
8524 $(modules['accountSwitcher'].accountMenu).attr('style','position: fixed;');
8525 }
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;');
8527 } else {
8528 if (typeof modules['accountSwitcher'].accountMenu !== 'undefined') {
8529 $(modules['accountSwitcher'].accountMenu).attr('style','position: fixed;');
8530 }
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;');
8532 }
8533 },
8534 pinHeader: function() {
8535 // Makes the Full header a fixed element
8536
8537 // the subreddit manager code changes the document's structure
8538 var sm = modules['subredditManager'].isEnabled();
8539
8540 var header = document.getElementById('header');
8541 if (header == null) return; // reddit is under heavy load
8542
8543 // add a dummy <div> to the document for spacing
8544 var spacer = document.createElement('div');
8545 spacer.id = 'RESPinnedHeaderSpacer';
8546
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";
8550
8551 // insert the spacer
8552 document.body.insertBefore(spacer, header.nextSibling);
8553
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);
8561
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);
8574 }, 10);
8575 },
8576 pinCommonElements: function(sm) {
8577 // pin the elements common to both pinHeader() and pinSubredditBar()
8578 if (sm) {
8579 // RES's subreddit menu
8580 RESUtils.addCSS('#RESSubredditGroupDropdown, #srList, #RESShortcutsAddFormContainer, #editShortcutDialog {position: fixed !important;}');
8581 } else {
8582 RESUtils.addCSS('#sr-more-link: {position: fixed;}');
8583 }
8584 },
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/';
8592 li.appendChild(a);
8593 tabmenu.appendChild(li);
8594 }
8595 };
8596
8597 modules['singleClick'] = {
8598 moduleID: 'singleClick',
8599 moduleName: 'Single Click Opener',
8600 category: 'UI',
8601 options: {
8602 openOrder: {
8603 type: 'enum',
8604 values: [
8605 { name: 'open comments then link', value: 'commentsfirst' },
8606 { name: 'open link then comments', value: 'linkfirst' }
8607 ],
8608 value: 'commentsfirst',
8609 description: 'What order to open the link/comments in.'
8610 },
8611 hideLEC: {
8612 type: 'boolean',
8613 value: false,
8614 description: 'Hide the [l=c] when the link is the same as the comments page'
8615 },
8616 openBackground: {
8617 type: 'boolean',
8618 value: false,
8619 description: 'Open the [l+c] link in background tabs'
8620 }
8621 },
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);
8625 },
8626 isMatchURL: function() {
8627 return RESUtils.isMatchURL(this.moduleID);
8628 },
8629 include: [
8630 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i,
8631 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\._]*\//i
8632 ],
8633 exclude: [
8634 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[-\w\._\/\?]*\/comments[-\w\._\/\?=]*/i
8635 ],
8636 beforeLoad: function() {
8637 if ((this.isEnabled()) && (this.isMatchURL())) {
8638 RESUtils.addCSS('.redditSingleClick { color: #888; font-weight: bold; cursor: pointer; padding: 0 1px; }');
8639 }
8640 },
8641 go: function() {
8642 if ((this.isEnabled()) && (this.isMatchURL())) {
8643 // do stuff here!
8644 this.applyLinks();
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);
8647 }
8648 },
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;
8661 }
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]';
8674 }
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) {
8680 e.preventDefault();
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;
8687 }
8688 if (BrowserDetect.isChrome()) {
8689 var thisJSON = {
8690 requestType: 'singleClick',
8691 linkURL: thisLink,
8692 openOrder: modules['singleClick'].options.openOrder.value,
8693 commentsURL: this.getAttribute('thisComments'),
8694 button: lcMouseBtn,
8695 ctrl: e.ctrlKey
8696 };
8697 chrome.extension.sendMessage(thisJSON);
8698 } else if (BrowserDetect.isSafari()) {
8699 var thisJSON = {
8700 requestType: 'singleClick',
8701 linkURL: thisLink,
8702 openOrder: modules['singleClick'].options.openOrder.value,
8703 commentsURL: this.getAttribute('thisComments'),
8704 button: lcMouseBtn,
8705 ctrl: e.ctrlKey
8706 }
8707 safari.self.tab.dispatchMessage("singleClick", thisJSON);
8708 } else if (BrowserDetect.isOpera()) {
8709 var thisJSON = {
8710 requestType: 'singleClick',
8711 linkURL: thisLink,
8712 openOrder: modules['singleClick'].options.openOrder.value,
8713 commentsURL: this.getAttribute('thisComments'),
8714 button: lcMouseBtn,
8715 ctrl: e.ctrlKey
8716 }
8717 opera.extension.postMessage(JSON.stringify(thisJSON));
8718 } else if (BrowserDetect.isFirefox()) {
8719 var thisJSON = {
8720 requestType: 'singleClick',
8721 linkURL: thisLink,
8722 openOrder: modules['singleClick'].options.openOrder.value,
8723 commentsURL: this.getAttribute('thisComments'),
8724 button: lcMouseBtn,
8725 ctrl: e.ctrlKey
8726 }
8727 self.postMessage(thisJSON);
8728 } else {
8729 var thisLink = $(this).parent().parent().parent().find('a.title').attr('href');
8730 if (!(thisLink.match(/^http/i))) {
8731 thisLink = 'http://' + document.domain + thisLink;
8732 }
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'));
8737 }
8738 window.open(thisLink);
8739 } else {
8740 window.open(thisLink);
8741 if (thisLink !== this.getAttribute('thisComments')) {
8742 // console.log('open comments');
8743 window.open(this.getAttribute('thisComments'));
8744 }
8745 }
8746 }
8747 }
8748 }, false);
8749 }
8750 }
8751 }
8752 }
8753 };
8754
8755
8756 modules['commentPreview'] = {
8757 moduleID: 'commentPreview',
8758 moduleName: 'Live Comment Preview',
8759 category: 'Comments',
8760 options: {
8761 enableBigEditor: {
8762 type: 'boolean',
8763 value: true,
8764 description: 'Enable the 2 column editor.'
8765 }
8766 },
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);
8770 },
8771 include: [
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
8781 ],
8782 isMatchURL: function() {
8783 return RESUtils.isMatchURL(this.moduleID);
8784 },
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; }');
8789 }
8790 },
8791 go: function() {
8792 if ((this.isEnabled()) && (this.isMatchURL())) {
8793 this.isWiki = $(document.body).is(".wiki-page");
8794 if (!this.isWiki) {
8795 this.converter = window.SnuOwnd.getParser();
8796 } else {
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
8807 });
8808
8809 this.tocConverter = SnuOwnd.getParser(SnuOwnd.getTocRenderer());
8810 }
8811
8812 this.bigTextTarget = null;
8813 if (this.options.enableBigEditor.value) {
8814 // Install the 2 column editor
8815 modules['commentPreview'].addBigEditor();
8816
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);
8821 }
8822 })
8823 }
8824 }
8825
8826 //Close the preview on submit
8827 $("body").delegate("form", {
8828 submit: function(e) {
8829 $(this).find(".livePreview").hide();
8830 }
8831 });
8832
8833 if (!this.isWiki) {
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);
8840 } else {
8841 this.attachWikiPreview()
8842 }
8843 }
8844 },
8845 markdownToHTML: function(md) {
8846 var body = this.converter.render(md);
8847 if (this.isWiki) {
8848 /*
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.
8851
8852 It would be nicer if they just used different functions for rendering the emphasis when making headers.</s>
8853
8854 It seems that my understanding was wrong, for some reason reddit doesn't even use snudown's TOC renderer.
8855 */
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;
8864 var prefix = 'wiki'
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();
8871 });
8872 if (!(aid in header_ids)) header_ids[aid] = 0;
8873 var id_num = header_ids[aid] + 1;
8874 header_ids[aid] += 1;
8875
8876 if (id_num > 1) aid = aid + id_num;
8877
8878 $(this).attr('id', aid);
8879
8880 var li = $('<li>').addClass(aid);
8881 var a = $('<a>').attr('href', '#'+aid).text(contents);
8882 li.append(a);
8883
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);
8889 parent = newUL;
8890 level++;
8891 } else if (level && thisLevel < previous) {
8892 while (level && parent.data('level') > thisLevel) {
8893 parent = parent.closest('ul');
8894 level -= 1;
8895 }
8896 }
8897 previous = thisLevel;
8898 parent.append(li);
8899 });
8900 doc.prepend(tocDiv);
8901 return doc.html()
8902 } else {
8903 return body;
8904 }
8905 },
8906 makeBigEditorButton: function() {
8907 return $('<button class="RESBigEditorPop" tabIndex="3">big editor</button>');
8908 },
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));
8913 }
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);
8919 }
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);
8927 preview.show();
8928 contents.html(html);
8929 } else {
8930 preview.hide();
8931 contents.html("");
8932 }
8933 });
8934 });
8935 });
8936 },
8937 attachWikiPreview: function() {
8938 if (modules['commentPreview'].options.enableBigEditor.value) {
8939 modules['commentPreview'].makeBigEditorButton().insertAfter($('.pageactions'));
8940 }
8941 var preview = modules["commentPreview"].makePreviewBox();
8942 preview.find(".md").addClass("wiki");
8943 preview.insertAfter($("#editform > br").first());
8944
8945 var contents = preview.find(".RESDialogContents");
8946 $("#wiki_page_content").bind("input", function() {
8947
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)
8953 preview.show();
8954 contents.html(html);
8955 } else {
8956 preview.hide();
8957 contents.html("");
8958 }
8959 });
8960 });
8961 },
8962 makePreviewBox: function() {
8963 return $("<div style=\"display: none\" class=\"RESDialogSmall livePreview\"><h3>Live Preview</h3><div class=\"md RESDialogContents\"></div></div>");
8964 },
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");
8973 if (len > max) {
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);
8981 } else {
8982 $('#BigEditor .errorList .error').hide().filter('.NO_TARGET').show();
8983 }
8984
8985 }));
8986 foot.append($('<button style="float:left;">close</button>').bind('click', modules.commentPreview.hideBigEditor));
8987
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>\
8992 </span>'));
8993
8994 contents.append(foot);
8995 left.append(contents);
8996
8997 var right = $('<div class="BERight RESDialogSmall"><h3>Preview</h3><div class="RESCloseButton RESFadeButton">&#xf04e;</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);
9000
9001 $(document.body).append(editor);
9002
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;
9009 },
9010 mouseout: function(e) {
9011 if (this.isFaded) $("#BigEditor").fadeTo(300, 1.0);
9012 $(document.body).addClass("RESScrollLock");
9013 this.isFaded = false;
9014 }
9015 });
9016 $('body').delegate('.RESBigEditorPop', 'click', modules.commentPreview.showBigEditor);
9017
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);
9025 }
9026 });
9027 }).bind("keydown", function(e) {
9028 //Close big editor on escape
9029 if (e.keyCode === modules["commentTools"].KEYS.ESCAPE) {
9030 modules["commentPreview"].hideBigEditor();
9031 e.preventDefault();
9032 return false;
9033 }
9034 });
9035 if (modules['commentTools'].isEnabled()) {
9036 contents.prepend(modules['commentTools'].makeEditBar());
9037 }
9038 },
9039 showBigEditor: function(e) {
9040 e.preventDefault();
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);
9046 var baseText;
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");
9051 } else {
9052 baseText = $('#wiki_page_content');
9053 $("#BigPreview").addClass("wiki");
9054 $(".BERight .RESDialogContents").addClass("wiki-page-content");
9055 }
9056
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;
9063 },
9064 hideBigEditor: function(quick, submitted) {
9065 if (quick === true) {
9066 $('#BigEditor').hide();
9067 } else {
9068 RESUtils.fadeElementOut(document.getElementById('BigEditor'), 0.3);
9069 }
9070 $('.side').removeClass('BESideHide');
9071 $('body').removeClass('RESScrollLock');
9072 var target = modules['commentPreview'].bigTextTarget;
9073
9074 if (target != null) {
9075 target.val($('#BigText').val())
9076 target.focus();
9077 if (submitted !== true) {
9078 var inputEvent = document.createEvent("HTMLEvents");
9079 inputEvent.initEvent("input", true, true);
9080 target[0].dispatchEvent(inputEvent);
9081 }
9082 modules['commentPreview'].bigTextTarget = null;
9083 }
9084 },
9085 };
9086
9087
9088
9089 modules['commentTools'] = {
9090 moduleID: 'commentTools',
9091 moduleName: 'Comment Tools',
9092 category: 'Comments',
9093 options: {
9094 commentingAs: {
9095 type: 'boolean',
9096 value: true,
9097 description: 'Shows your currently logged in username to avoid posting from the wrong account.'
9098 },
9099 userAutocomplete: {
9100 type: 'boolean',
9101 value: true,
9102 description: 'Show user autocomplete tool when typing in posts, comments and replies'
9103 },
9104 subredditAutocomplete: {
9105 type: 'boolean',
9106 value: true,
9107 description: 'Show subreddit autocomplete tool when typing in posts, comments and replies'
9108 },
9109 showInputLength: {
9110 type: 'boolean',
9111 value: true,
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.'
9113 },
9114 keyboardShortcuts: {
9115 type: 'boolean',
9116 value: true,
9117 description: 'Use keyboard shortcuts to apply styles to selected text'
9118 },
9119 macros: {
9120 type: 'table',
9121 addRowText: '+add shortcut',
9122 fields: [
9123 { name: 'label', type: 'text' },
9124 { name: 'text', type: 'textarea' },
9125 { name: 'category', type: 'text' },
9126 { name: 'key', type: 'keycode' }
9127 ],
9128 value: [
9129 ],
9130 description: "Add buttons to insert frequently used snippets of text."
9131 },
9132 keepMacroListOpen: {
9133 type: 'boolean',
9134 value: false,
9135 description: 'After selecting a macro from the dropdown list, do not hide the list.'
9136 }
9137 },
9138 description: 'Provides shortcuts for easier markdown.',
9139 isEnabled: function() {
9140 return RESConsole.getModulePrefs(this.moduleID);
9141 },
9142 include: [
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
9152 ],
9153 isMatchURL: function() {
9154 return RESUtils.isMatchURL(this.moduleID);
9155 },
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; }');
9172 }
9173 },
9174 SUBMIT_LIMITS: {
9175 STYLESHEET: 128*1024,
9176 SIDEBAR: 5120,
9177 DESCRIPTION: 500,
9178 WIKI: 256*1024,
9179 POST: 10000,
9180 POST_TITLE: 300
9181 },
9182 //Moved this out of go() because the large commentPreview may need it.
9183 macroCallbackTable: [],
9184 macroKeyTable: [],
9185 go: function() {
9186 if ((this.isEnabled()) && (this.isMatchURL())) {
9187 this.isWiki = $(document.body).is(".wiki-page");
9188 this.migrateData();
9189 $("body").delegate("li.viewSource a", {
9190 click: function(e) {
9191 e.preventDefault();
9192 modules["commentTools"].viewSource(this);
9193 }
9194 }).delegate(".usertext-edit.viewSource .cancel", {
9195 click: function() {
9196 $(this).parents(".usertext-edit.viewSource").hide();
9197
9198 }
9199 }).delegate("div.markdownEditor a", {
9200 click: function(e) {
9201 e.preventDefault();
9202
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];
9206 if (box == null) {
9207 console.error("Failed to locate textarea.");
9208 return;
9209 }
9210 var handler = modules["commentTools"].macroCallbackTable[index];
9211 if (box == null) {
9212 console.error("Failed to locate find callback.");
9213 return;
9214 }
9215 handler.call(modules["commentTools"], this, box);
9216
9217 box.focus();
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);
9222 }
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"
9229 }).show();
9230 }
9231 }).delegate(".RESMacroDropdown", {
9232 mouseleave: function(e) {
9233 $(this).hide();
9234 }
9235 });
9236
9237 if (this.options.showInputLength.value) {
9238
9239 $("body").delegate(".usertext-edit textarea, #title-field textarea, #BigEditor textarea, #wiki_page_content", {
9240 input: function(e){
9241 modules['commentTools'].updateCounter(this);
9242 }
9243 });
9244 }
9245
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.
9253 $(this).blur();
9254 e.preventDefault();
9255 return false;
9256 } else {
9257 return true;
9258 }
9259 }
9260
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);
9271 e.preventDefault();
9272 return false;
9273 }
9274 };
9275 }
9276 });
9277 }
9278 if (this.options.subredditAutocomplete.value || this.options.userAutocomplete.value) {
9279 this.addAutoCompletePop();
9280 }
9281
9282 //Perform initial setup of tools over the whole page
9283 this.attachCommentTools();
9284 this.attatchViewSourceButtons()
9285 /*
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);
9291 */
9292 RESUtils.watchForElement("newComments", modules["commentTools"].attatchViewSourceButtons);
9293 }
9294 },
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;
9306 }
9307 if (typeof previewOptions.keyboardShortcuts !== "undefined") {
9308 this.options.keyboardShortcuts.value = previewOptions.keyboardShortcuts.value
9309 delete previewOptions.keyboardShortcuts;
9310 }
9311 if (typeof previewOptions.subredditAutocomplete !== "undefined") {
9312 this.options.subredditAutocomplete.value = previewOptions.subredditAutocomplete.value
9313 delete previewOptions.subredditAutocomplete;
9314 }
9315 if (typeof previewOptions.macros !== "undefined") {
9316 var macros;
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("");
9320 }
9321 delete previewOptions.macros;
9322 }
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);
9326 } else {
9327 //No migration will be performed
9328 RESStorage.setItem("RESmodules.commentTools.macroDataVersion", LATEST_MACRO_DATA_VERSION);
9329 }
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("");
9334 }
9335 RESStorage.setItem("RESmodules.commentTools.macroDataVersion", LATEST_MACRO_DATA_VERSION);
9336 }
9337 },
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>');
9346 });
9347 }
9348 },
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) {
9353 sourceDiv.toggle();
9354 } else {
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];
9359
9360 var isSelfText = permaLink.is(".comments");
9361 if (jsonURL.indexOf('?context') !== -1) {
9362 jsonURL = jsonURL.replace('?context=3','.json?');
9363 } else {
9364 jsonURL += '/.json';
9365 }
9366 this.gettingSource = this.gettingSource || {};
9367 if (this.gettingSource[postID]) return;
9368 this.gettingSource[postID] = true;
9369
9370 GM_xmlhttpRequest({
9371 method: "GET",
9372 url: jsonURL,
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>');
9376 if (!isSelfText) {
9377 var sourceText = null;
9378 if (typeof thisResponse[1] !== 'undefined') {
9379 sourceText = thisResponse[1].data.children[0].data.body;
9380 } else {
9381 var thisData = thisResponse.data.children[0].data;
9382 if (thisData.id == postID) {
9383 sourceText = thisData.body;
9384 } else {
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;
9391 break;
9392 }
9393 }
9394 }
9395 }
9396 // sourceText in this case is reddit markdown. escaping it would screw it up.
9397 userTextForm.find("textarea[name=text]").html(sourceText);
9398 } else {
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);
9403 }
9404 buttonList.before(userTextForm);
9405 }
9406 });
9407 }
9408 },
9409 attachCommentTools: function (elem) {
9410 if (elem == null) elem = document.body;
9411 $(elem).find("textarea[name]").each(modules["commentTools"].attachEditorToUsertext);
9412 },
9413 attachEditorToUsertext: function() {
9414 if (this.hasAttribute("data-max-length")) return;
9415 var limit;
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;
9424 default:
9425 // console.warn("unhandled form", this);
9426 return;
9427 }
9428 $(this).attr("data-max-length", limit);
9429
9430
9431 if (this.name === "title") return;
9432
9433 var bar = modules['commentTools'].makeEditBar();
9434 if (this.id === "wiki_page_content") {
9435 $(this).parent().prepend(bar);
9436 } else {
9437 $(this).parent().before(bar);
9438 }
9439 modules['commentTools'].updateCounter(this);
9440 },
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');
9449 } else {
9450 counter.removeClass('tooLong');
9451 }
9452 },
9453 makeEditBar: function() {
9454 if (this.cachedEditBar != null) {
9455 return $(this.cachedEditBar).clone();
9456 }
9457
9458 var editBar = $('<div class="markdownEditor">');
9459
9460 editBar.append(this.makeEditButton("<b>Bold</b>", "ctrl-b", [66, false, true, false, false], function(button, box) {
9461 this.wrapSelection(box, "**", "**");
9462 }));
9463 editBar.append(this.makeEditButton("<i>Italic</i>", "ctrl-i", [73, false, true, false, false], function(button, box) {
9464 this.wrapSelection(box, "*", "*");
9465 }));
9466 editBar.append(this.makeEditButton("<del>strike</del>", "ctrl-s", 83, function(button, box) {
9467 this.wrapSelection(box, "~~", "~~");
9468 }));
9469 editBar.append(this.makeEditButton("<sup>sup</sup>", "", null, function(button, box) {
9470 this.wrapSelectedWords(box, "^");
9471 }));
9472 editBar.append(this.makeEditButton("Link", "", null, function(button, box) {
9473 this.linkSelection(box);
9474 }));
9475 editBar.append(this.makeEditButton(">Quote", "", null, function(button, box) {
9476 this.wrapSelectedLines(box, "> ", "");
9477 }));
9478 editBar.append(this.makeEditButton("<span style=\"font-family: Courier New\">Code</span>", "", null, function(button, box) {
9479 this.wrapSelectedLines(box, " ", "");
9480 }));
9481 editBar.append(this.makeEditButton("&bull;Bullets", "", null, function(button, box) {
9482 this.wrapSelectedLines(box, "* ", "");
9483 }));
9484 editBar.append(this.makeEditButton("1.Numbers", "", null, function(button, box) {
9485 this.wrapSelectedLines(box, "1. ", "");
9486 }));
9487
9488 if (modules["commentTools"].options.showInputLength.value) {
9489 var counter = $('<span class="RESCharCounter" title="character limit: 0/?????">0/?????</span>');
9490 editBar.append(counter);
9491 }
9492
9493 this.addButtonToMacroGroup("", this.makeEditButton("reddiquette", "", null, function(button, box){
9494 var clickCount = $(button).data("clickCount") || 0;
9495 clickCount++;
9496 $(button).data("clickCount", clickCount);
9497 if (clickCount > 2) {
9498 $(button).hide();
9499 }
9500 this.macroSelection(box, "[reddiquette](http://www.reddit.com/help/reddiquette) ", "");
9501 }));
9502
9503 this.addButtonToMacroGroup("", this.makeEditButton("[Promote]", "", null, function(button, box){
9504 var clickCount = $(button).data("clickCount") || 0;
9505 clickCount++;
9506 $(button).data("clickCount", clickCount);
9507 if (clickCount > 2) {
9508 $(button).hide();
9509 modules["commentTools"].lod();
9510 }
9511 this.macroSelection(box, "[Reddit Enhancement Suite](http://redditenhancementsuite.com) ");
9512 }));
9513
9514 this.addButtonToMacroGroup("", this.makeEditButton("&#3232;\_&#3232;", "Look of disaproval", null, function(button, box) {
9515 this.macroSelection(box, "&#3232;\_&#3232;");
9516 }));
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);
9521 });
9522 modules['commentTools'].addButtonToMacroGroup('', addMacroButton);
9523
9524
9525
9526
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);
9533
9534 }
9535 this.cachedEditBar = wrappedEditBar;
9536 return this.cachedEditBar;
9537 },
9538 macroDropDownTable: {},
9539 getMacroGroup: function(groupName) {
9540 //Normalize and supply a default group name{}
9541 groupName = (groupName||"").toString().trim() || "macros";
9542 var macroGroup;
9543 if (groupName in this.macroDropDownTable) {
9544 macroGroup = this.macroDropDownTable[groupName];
9545 } else {
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);
9551 }
9552 return macroGroup;
9553 },
9554 addButtonToMacroGroup: function(groupName, button) {
9555 var group = this.getMacroGroup(groupName);
9556 group.dropdown.append($("<li>").append(button));
9557 },
9558 buildMacroDropdowns: function(editBar) {
9559 var macros = this.options.macros.value;
9560
9561 for (var i = 0; i < macros.length; i++) {
9562 var macro = macros[i];
9563
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, "");
9568 });
9569 this.addButtonToMacroGroup(category, button);
9570 }).apply(this, macro);
9571 }
9572
9573
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);
9578 }
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);
9583 }
9584 editBar.append(macroWrapper);
9585 },
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({
9591 title: title,
9592 href: "javascript:void(0)",
9593 tabindex: 1,
9594 "data-macro-index": macroButtonIndex
9595 });
9596
9597 if (key != null && key[0] != null) {
9598 this.macroKeyTable.push([key, macroButtonIndex]);
9599 }
9600 this.macroCallbackTable[macroButtonIndex] = handler;
9601 return button;
9602 },
9603 linkSelection: function(box) {
9604 var url = prompt("Enter the URL:", "");
9605 if (url != null) {
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(/\)/, "\\)");
9615 return text;
9616 });
9617 }
9618 },
9619 macroSelection: function(box, macroText) {
9620 if (!this.options.keepMacroListOpen.value) $('.RESMacroDropdown').fadeOut(100);
9621 this.wrapSelection(box, macroText, "");
9622 },
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;
9627
9628 //We will restore the selection later, so record the current selection.
9629 var selectionStart = box.selectionStart;
9630 var selectionEnd = box.selectionEnd;
9631
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);
9636
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 += " ";
9642 cursor--;
9643 }
9644 selectedText = selectedText.substring(0, cursor+1);
9645
9646 if (escapeFunction != null) {
9647 selectedText = escapeFunction(selectedText);
9648 }
9649
9650 box.value = beforeSelection + prefix + selectedText + suffix + trailingSpace + afterSelection;
9651
9652 box.selectionStart = selectionStart + prefix.length;
9653 box.selectionEnd = selectionEnd + prefix.length;
9654
9655 box.scrollTop = scrollTop;
9656 },
9657 wrapSelectedLines: function(box, prefix, suffix) {
9658 var scrollTop = box.scrollTop;
9659 var selectionStart = box.selectionStart;
9660 var selectionEnd = box.selectionEnd;
9661
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;
9681
9682 selectionStart += startMovement;
9683 selectionEnd += endMovement;
9684 lineStart += prefix.length;
9685 lineEnd += prefix.length + suffix.length;
9686 }
9687 //Remember the newline
9688 startPosition = lineEnd + 1;
9689 }
9690
9691 box.value = lines.join("\n");
9692 box.selectionStart = selectionStart;
9693 box.selectionEnd = selectionEnd;
9694 box.scrollTop = scrollTop;
9695 },
9696 wrapSelectedWords: function(box, prefix) {
9697 var scrollTop = box.scrollTop;
9698 var selectionStart = box.selectionStart;
9699 var selectionEnd = box.selectionEnd;
9700
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);
9705
9706 var selectionModify = 0;
9707
9708 for (i = 0; i < selectedWords.length; i++) {
9709 if (selectedWords[i] !== "") {
9710 if (selectedWords[i].indexOf("\n") !== -1)
9711 {
9712 newLinePosition = selectedWords[i].lastIndexOf("\n") + 1;
9713 selectedWords[i] = selectedWords[i].substring(0, newLinePosition) + prefix + selectedWords[i].substring(newLinePosition);
9714 selectionModify += prefix.length;
9715 }
9716 if (selectedWords[i].charAt(0) !== "\n") {
9717 selectedWords[i] = prefix + selectedWords[i];
9718 }
9719 selectionModify += prefix.length;
9720 }
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;
9726 }
9727 }
9728
9729 box.value = beforeSelection + selectedWords.join(" ") + afterSelection;
9730 box.selectionStart = selectionStart;
9731 box.selectionEnd = selectionEnd + selectionModify;
9732 box.scrollTop = scrollTop;
9733 },
9734 lod: function() {
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;">&#3232;\_&#3232;</div> when you do this, people direct their frustrations at <b>me</b>... could we please maybe give this a rest?</div></div>');
9738 }
9739 $('#RESlod').fadeIn('slow', function() {
9740 setTimeout(function() {
9741 $('#RESlod').fadeOut('slow');
9742 }, 5000);
9743 });
9744 },
9745 KEYS: {
9746 BACKSPACE: 8, TAB: 9, ENTER: 13,
9747 ESCAPE: 27, SPACE: 32,
9748 PAGE_UP: 33, PAGE_DOWN: 34,
9749 END: 35, HOME: 36,
9750 LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40,
9751 NUMPAD_ENTER: 108, COMMA: 188
9752 },
9753 addAutoCompletePop: function() {
9754
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) {
9760
9761 modules["commentTools"].autoCompleteHideDropdown();
9762 modules["commentTools"].autoCompleteInsert(this.innerHTML);
9763 });
9764 $("body").append(this.autoCompletePop);
9765
9766 $("body").delegate(".usertext .usertext-edit textarea, #BigText, #wiki_page_content", {
9767 keyup: this.autoCompleteTrigger,
9768 keydown: this.autoCompleteNavigate,
9769 blur: this.autoCompleteHideDropdown
9770 });
9771 },
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;
9787 }
9788
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);
9792 if (match) {
9793 mod.autoCompleteInsert(match[2]);
9794 }
9795 }
9796 return mod.autoCompleteHideDropdown();
9797 }
9798
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]);
9805 }
9806
9807 RESUtils.debounce("autoComplete", 300, function() {
9808 if (type === "r") {
9809 mod.getSubredditCompletions(query);
9810 } else if (type === "u") {
9811 mod.getUserCompletions(query);
9812 }
9813 });
9814 },
9815 getSubredditCompletions: function(query) {
9816 var mod = modules['commentTools'];
9817 if (this.options.subredditAutocomplete.value) {
9818 $.ajax({
9819 type: "POST",
9820 url: "/api/search_reddit_names.json",
9821 data: {query: query, app: "res"},
9822 dataType: "json",
9823 success: function(data) {
9824 mod.autoCompleteCache['r/'+query] = data.names;
9825 mod.autoCompleteUpdateDropdown(data.names);
9826 mod.autoCompleteSetNavIndex(0);
9827 }
9828 });
9829 }
9830 },
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) {
9836 return e.innerHTML;
9837 });
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) {
9844 prev.push(current);
9845 }
9846 return prev;
9847 }, []);
9848
9849 this.autoCompleteCache['u/'+query] = names;
9850 this.autoCompleteUpdateDropdown(names);
9851 this.autoCompleteSetNavIndex(0);
9852 }
9853 },
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) {
9863 case KEYS.DOWN:
9864 case KEYS.RIGHT:
9865 e.preventDefault();
9866 if (index < entries.length-1) index++;
9867 mod.autoCompleteSetNavIndex(index);
9868 break;
9869 case KEYS.UP:
9870 case KEYS.LEFT:
9871 e.preventDefault();
9872 if (index > 0) index--;
9873 mod.autoCompleteSetNavIndex(index);
9874 break;
9875 case KEYS.TAB:
9876 case KEYS.ENTER:
9877 e.preventDefault();
9878 $(entries[index]).click();
9879 break;
9880 case KEYS.ESCAPE:
9881 e.preventDefault();
9882 mod.autoCompleteHideDropdown();
9883 return false;
9884 break;
9885 }
9886 }
9887 },
9888 autoCompleteSetNavIndex: function(index) {
9889 var entries = modules["commentTools"].autoCompletePop.find("a.choice");
9890 entries.removeClass("selectedItem");
9891 entries.eq(index).addClass("selectedItem");
9892 },
9893 autoCompleteHideDropdown: function() {
9894 modules["commentTools"].autoCompletePop.hide();
9895 },
9896 autoCompleteUpdateDropdown: function(names) {
9897 var mod = modules["commentTools"];
9898
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>');
9903 });
9904
9905 var textareaOffset = $(mod.autoCompleteLastTarget).offset();
9906 textareaOffset.left += $(mod.autoCompleteLastTarget).width();
9907 mod.autoCompletePop.css(textareaOffset).show();
9908
9909 mod.autoCompleteSetNavIndex(0);
9910
9911 },
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;
9920 textarea.focus()
9921 },
9922 findTextareaForElement: function(elem) {
9923 return $(elem)
9924 .closest(".usertext-edit, .RESDialogContents, .wiki-page-content")
9925 .find("textarea")
9926 .filter("#BigText, [name=text], [name=description], [name=public_description], #wiki_page_content")
9927 .first();
9928 }
9929 };
9930
9931
9932 modules['usernameHider'] = {
9933 moduleID: 'usernameHider',
9934 moduleName: 'Username Hider',
9935 category: 'Accounts',
9936 options: {
9937 displayText: {
9938 type: 'text',
9939 value: '~anonymous~',
9940 description: 'What to replace your username with, default is ~anonymous~'
9941 }
9942 },
9943 description: 'This module hides your real username when you\'re logged in to reddit.',
9944 isEnabled: function() {
9945 return RESConsole.getModulePrefs(this.moduleID);
9946 },
9947 include: [
9948 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i,
9949 /^https?:\/\/reddit\.com\/[-\w\.\/]*/i
9950 ],
9951 isMatchURL: function() {
9952 return RESUtils.isMatchURL(this.moduleID);
9953 },
9954 beforeLoad: function() {
9955 if ((this.isEnabled()) && (this.isMatchURL())) {
9956 if (!RESUtils.loggedInUser(true)) {
9957 this.tryAgain = true;
9958 return false;
9959 }
9960 this.hideUsername();
9961 }
9962 },
9963 go: function() {
9964 if ((this.isEnabled()) && (this.isMatchURL())) {
9965 if (this.tryAgain && RESUtils.loggedInUser()) {
9966 this.hideUsername();
9967 GM_addStyle(RESUtils.css);
9968 }
9969 }
9970 },
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+';}');
9983 }
9984 if ( curatedBy && curatedBy.href.slice(-(user.length+1)) === '/' + user ){
9985 curatedBy.textContent = 'curated by /u/' + this.options.displayText.value;
9986 }
9987 }
9988 };
9989
9990
9991 /* siteModule format:
9992 name: {
9993 //Initialization method for things that cannot be performed inline. The method
9994 //is required to be present, but it can be empty
9995 go: function(){},
9996
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;},
10001
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) {},
10008
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) {}
10017 */
10018 /*
10019 Embedding infomation:
10020 all embedding information (except 'site') is to be attatched the
10021 html anchor in the handleInfo function
10022
10023 required type:
10024 'IMAGE' for single images | 'GALLERY' for image galleries | 'TEXT' html/text to be displayed
10025 required src:
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).
10035 optional caption:
10036 string to be displayed below the image
10037 optional credits:
10038 string to be displayed below caption
10039 optional galleryStart:
10040 zero-indexed page number to open the gallery to
10041 */
10042 modules['showImages'] = {
10043 moduleID: 'showImages',
10044 moduleName: 'Inline Image Viewer',
10045 category: 'UI',
10046 options: {
10047 maxWidth: {
10048 type: 'text',
10049 value: '640',
10050 description: 'Max width of image displayed onscreen'
10051 },
10052 maxHeight: {
10053 type: 'text',
10054 value: '480',
10055 description: 'Max height of image displayed onscreen'
10056 },
10057 openInNewWindow: {
10058 type: 'boolean',
10059 value: true,
10060 description: 'Open images in a new tab/window when clicked?'
10061 },
10062 hideNSFW: {
10063 type: 'boolean',
10064 value: false,
10065 description: 'If checked, do not show images marked NSFW.'
10066 },
10067 autoExpandSelfText: {
10068 type: 'boolean',
10069 value: true,
10070 description: 'When loading selftext from an Aa+ expando, auto reveal images.'
10071 },
10072 imageZoom: {
10073 type: 'boolean',
10074 value: true,
10075 description: 'Allow dragging to resize/zoom images.'
10076 },
10077 markVisited: {
10078 type: 'boolean',
10079 value: true,
10080 description: 'Mark links visited when you view images (does eat some resources).'
10081 },
10082 sfwHistory: {
10083 type: 'enum',
10084 value: 'add',
10085 values: [
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'}
10089 ],
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>'
10095 },
10096 ignoreDuplicates: {
10097 type: 'boolean',
10098 value: true,
10099 description: 'Do not create expandos for images that appear multiple times in a page.'
10100 },
10101 displayImageCaptions: {
10102 type: 'boolean',
10103 value: true,
10104 description: 'Retrieve image captions/attribution information.'
10105 },
10106 loadAllInAlbum: {
10107 type: 'boolean',
10108 value: false,
10109 description: 'Display all images at once in a \'filmstrip\' layout, rather than the default navigable \'slideshow\' style.'
10110 }
10111 },
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);
10115 },
10116 include: [
10117 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\_\?=]*/i
10118 ],
10119 exclude: [
10120 /^https?:\/\/([a-z]+)\.reddit\.com\/ads\/[-\w\.\_\?=]*/i,
10121 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*\/submit\/?$/i
10122 ],
10123 isMatchURL: function() {
10124 return RESUtils.isMatchURL(this.moduleID);
10125 },
10126 beforeLoad: function() {
10127 if ((this.isEnabled()) && (this.isMatchURL())) {
10128 if (!this.options.displayImageCaptions.value) {
10129 RESUtils.addCSS('.imgTitle, .imgCaptions { display: none; }');
10130 }
10131 }
10132 },
10133 go: function() {
10134 if ((this.isEnabled()) && (this.isMatchURL())) {
10135
10136 this.imageList = [];
10137 this.imagesRevealed = {};
10138 this.dupeAnchors = 0;
10139 /*
10140 true: show all images
10141 false: hide all images
10142 'any string': display images match the tab
10143 */
10144 this.currentImageTab = false;
10145 this.customImageTabs = {};
10146
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);
10153 }, false);
10154 this.imageTrackFrame.style.display = 'none';
10155 this.imageTrackFrame.style.width = '0px';
10156 this.imageTrackFrame.style.height = '0px';
10157 document.body.appendChild(this.imageTrackFrame);
10158 }
10159 this.imageTrackStack = [];
10160 }
10161
10162 //set up all site modules
10163 for (var key in this.siteModules) {
10164 this.siteModules[key].go();
10165 }
10166 this.scanningForImages = false;
10167
10168 RESUtils.watchForElement('siteTable', modules['showImages'].findAllImages);
10169 RESUtils.watchForElement('selfText', modules['showImages'].findAllImagesInSelfText);
10170 RESUtils.watchForElement('newComments', modules['showImages'].findAllImagesInSelfText);
10171
10172 this.createImageButtons();
10173 this.findAllImages();
10174 document.addEventListener('dragstart', function(){return false;}, false);
10175 }
10176 },
10177 findAllImagesInSelfText: function(ele) {
10178 modules['showImages'].findAllImages(ele, true);
10179 },
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');
10183 if (hbl) {
10184 var mainMenuUL = document.createElement('ul');
10185 mainMenuUL.setAttribute('class','tabmenu viewimages');
10186 mainMenuUL.setAttribute('style','display: inline-block');
10187 hbl.appendChild(mainMenuUL);
10188 }
10189 } else {
10190 var mainMenuUL = document.body.querySelector('#header-bottom-left ul.tabmenu');
10191 }
10192 if (mainMenuUL) {
10193 var li = document.createElement('li');
10194 var a = document.createElement('a');
10195 var text = document.createTextNode('scanning for images...');
10196 this.scanningForImages = true;
10197
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');
10204 }
10205 }, true);
10206 a.appendChild(text);
10207 li.appendChild(a);
10208 mainMenuUL.appendChild(li);
10209 this.viewImageButton = a;
10210 /*
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)
10218
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.
10223
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'`.
10225
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.
10227
10228 Examples:
10229 A hypothetical setup for /r/minecraft that creates tabs for builds, mods, and texture packs:
10230
10231 [](#/RES_SR_Config/ImageTabs?build=build,project&mod=mod&texture%20pack=texture,textures,pack,texture%20pack)
10232
10233 To duplicate the behavior originally used for /r/gonewild you would use:
10234
10235 [](#/RES_SR_Config/ImageTabs?m=m,man,male&f=f,fem,female)
10236
10237 */
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];
10242 }
10243
10244
10245 if (tabConfig) {
10246 var switches = {};
10247 var switchCount = 0;
10248
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);
10264 }
10265 if (acceptedParts.length > 0) {
10266 if (!(label in switches)) switchCount++;
10267 switches[label] = acceptedParts;
10268 }
10269 }
10270 }
10271 if (switchCount > 0) {
10272 for (var key in switches) {
10273 this.customImageTabs[key] = new RegExp('[\\[\\{\\<\\(]\s*('+switches[key].join('|')+')\s*[\\]\\}\\>\\)]','i');
10274 }
10275 }
10276 }
10277
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);
10289 };
10290 })(mode), true);
10291
10292 a.appendChild(text);
10293 li.appendChild(a);
10294 mainMenuUL.appendChild(li);
10295 }
10296 }
10297 }
10298 },
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;
10306 } else {
10307 this.currentImageTab = true;
10308 }
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;
10315 } else {
10316 //Otherwise ignore it
10317 return;
10318 }
10319 this.updateImageButtons();
10320 this.updateRevealedImages(type);
10321 },
10322 updateImageButtons: function() {
10323 var imgCount = this.imageList.length;
10324 var showHideText = 'view';
10325 if (this.currentImageTab == true) {
10326 showHideText = 'hide';
10327 }
10328 if (typeof this.viewImageButton !== 'undefined') {
10329 var buttonText = showHideText + ' images ';
10330 if (! RESUtils.currentSubreddit('dashboard')) buttonText += '(' + imgCount + ')';
10331 $(this.viewImageButton).text(buttonText);
10332 }
10333 },
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));
10339 }
10340 }
10341 },
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);
10350 }
10351 //If false then there is no need to go through the NSFW filter
10352 if (!isMatched) return false;
10353
10354 image.NSFW = false;
10355 if (this.options.hideNSFW.value) {
10356 image.NSFW = /nsfw/i.test(image.text);
10357 }
10358
10359 return !image.NSFW;
10360 },
10361 findAllImages: function(elem, isSelfText) {
10362 modules['showImages'].scanningForImages = true;
10363 if (elem == null) {
10364 elem = document.body;
10365 }
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;
10378 } else {
10379 allElements = elem.querySelectorAll('#siteTable A.title');
10380 }
10381
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);
10389 }
10390 });
10391 } else {
10392 var chunkLength = allElements.length;
10393 for (var i = 0; i < chunkLength; i++) {
10394 modules['showImages'].checkElementForImage(allElements[i]);
10395 }
10396 modules['showImages'].scanningSelfText = false;
10397 modules['showImages'].scanningForImages = false;
10398 modules['showImages'].updateImageButtons(modules['showImages'].imageList.length);
10399 }
10400 },
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');
10405 }
10406 } else {
10407 elem.NSFW = false;
10408 }
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';
10416 }
10417 if (!siteFound) {
10418 for (var site in this.siteModules) {
10419 if (site === 'default') continue;
10420 if (this.siteModules[site].detect(elem)) {
10421 elem.site = site;
10422 siteFound = true;
10423 break;
10424 }
10425 }
10426 }
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);
10433 });
10434 }
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);
10440 }
10441 },
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];
10449
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;
10459
10460 if (elem.type === 'GALLERY' && elem.src && elem.src.length) expandLink.setAttribute('title', elem.src.length + ' items in gallery');
10461 $(expandLink).html('&nbsp;');
10462 expandLink.addEventListener('click', function(e) {
10463 e.preventDefault();
10464 modules['showImages'].revealImage(e.target, (e.target.classList.contains('collapsedExpando')));
10465 }, true);
10466 var preNode = null;
10467 if (elem.parentNode.classList.contains('title')) {
10468 preNode = elem.parentNode;
10469 expandLink.classList.add('linkImg');
10470 } else {
10471 preNode = elem;
10472 expandLink.classList.add('commentImg');
10473 }
10474 insertAfter(preNode, expandLink);
10475 /*
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
10479 */
10480 expandLink.imageLink = elem;
10481 mod.imageList.push(expandLink);
10482
10483 if (mod.scanningSelfText && mod.options.autoExpandSelfText.value) {
10484 mod.revealImage(expandLink, true);
10485 } else {
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));
10489
10490
10491 }
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);
10495 }
10496 },
10497 revealImage: function(expandoButton, showHide) {
10498 if ((!expandoButton) || (! $(expandoButton).is(':visible'))) return false;
10499 // showhide = false means hide, true means show!
10500
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);
10504 return;
10505 }
10506 if (expandoButton.expandoBox && expandoButton.expandoBox.classList.contains('madeVisible')) {
10507 if (!showHide) {
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);
10512 mediaTag.pause();
10513 }
10514 } else {
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);
10520 }
10521 }
10522 this.handleSRStyleToggleVisibility();
10523 } else if (showHide) {
10524 //TODO: flash, custom
10525 switch (imageLink.type) {
10526 case 'IMAGE':
10527 case 'GALLERY':
10528 this.generateImageExpando(expandoButton);
10529 break;
10530 case 'TEXT':
10531 this.generateTextExpando(expandoButton);
10532 break;
10533 case 'VIDEO':
10534 this.generateVideoExpando(expandoButton, imageLink.mediaOptions);
10535 break;
10536 case 'AUDIO':
10537 this.generateAudioExpando(expandoButton);
10538 break;
10539 case 'NOEMBED':
10540 this.generateNoEmbedExpando(expandoButton);
10541 break;
10542 }
10543 }
10544 },
10545 generateImageExpando: function(expandoButton) {
10546 var imageLink = expandoButton.imageLink;
10547 var which = imageLink.galleryStart || 0;
10548
10549 var imgDiv = document.createElement('div');
10550 imgDiv.classList.add('madeVisible');
10551 imgDiv.currentImage = which;
10552 imgDiv.sources = [];
10553
10554 // Test for a single image or an album/array of image
10555 if (Array.isArray(imageLink.src)) {
10556 imgDiv.sources = imageLink.src;
10557
10558 // Also preload images for an album
10559 this.preloadImages(imageLink.src, 0);
10560 } else {
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;
10564 }
10565
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);
10571 }
10572
10573 if ('imgCaptions' in imageLink) {
10574 var captions = document.createElement('div');
10575 captions.className = 'imgCaptions';
10576 $(captions).safeHtml(imageLink.caption);
10577 imgDiv.appendChild(captions);
10578 }
10579
10580 if ('credits' in imageLink) {
10581 var credits = document.createElement('div');
10582 credits.className = 'imgCredits';
10583 $(credits).safeHtml(imageLink.credits);
10584 imgDiv.appendChild(credits);
10585 }
10586
10587 switch(imageLink.type){
10588 case 'GALLERY':
10589 if (this.options.loadAllInAlbum.value) {
10590 if (imgDiv.sources.length > 1) {
10591 var albumLength = " (" + imgDiv.sources.length + " images)";
10592 $(header).append(albumLength);
10593 }
10594
10595 for (var imgNum = 0; imgNum < imgDiv.sources.length; imgNum++) {
10596 addImage(imgDiv, imgNum, this);
10597 }
10598 break;
10599 } else {
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';
10603
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;
10610 } else {
10611 topWrapper.currentImage -= 1;
10612 }
10613 adjustGalleryDisplay(topWrapper);
10614 });
10615 controlWrapper.appendChild(leftButton);
10616
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;
10622 } else {
10623 posLabel.textContent = "Whoops, this gallery seems to be empty.";
10624 }
10625 controlWrapper.appendChild(posLabel);
10626
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;
10633 } else {
10634 topWrapper.currentImage += 1;
10635 }
10636 adjustGalleryDisplay(topWrapper);
10637 });
10638 controlWrapper.appendChild(rightButton);
10639
10640 if (!imgDiv.sources.length) {
10641 $(leftButton).css('visibility','hidden');
10642 $(rightButton).css('visibility','hidden');
10643 }
10644
10645 imgDiv.appendChild(controlWrapper);
10646 }
10647
10648 case 'IMAGE':
10649 addImage(imgDiv, which, this);
10650 }
10651
10652 function addImage(container, sourceNumber, thisHandle) {
10653 var sourceImage = container.sources[sourceNumber];
10654
10655 var paragraph = document.createElement('p');
10656
10657 if (!sourceImage) {
10658 return;
10659 }
10660 if ('title' in sourceImage) {
10661 var imageTitle = document.createElement('h4');
10662 imageTitle.className = 'imgCaptions';
10663 $(imageTitle).safeHtml(sourceImage.title);
10664 paragraph.appendChild(imageTitle);
10665 }
10666
10667 if ('caption' in sourceImage) {
10668 var imageCaptions = document.createElement('div');
10669 imageCaptions.className = 'imgCaptions';
10670 $(imageCaptions).safeHtml(sourceImage.caption);
10671 paragraph.appendChild(imageCaptions);
10672 }
10673
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';
10679 }
10680
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");
10686 };
10687 image.onload = function() {
10688 image.classList.remove("RESImageError");
10689 };
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);
10701
10702 container.appendChild(paragraph);
10703 }
10704
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);
10716 } else {
10717 topLevel.querySelector('.RESGalleryLabel').textContent = "Whoops, this gallery seems to be empty.";
10718 }
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');
10725 } else {
10726 leftButton.classList.remove('end');
10727 rightButton.classList.remove('end');
10728 }
10729
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);
10735 }
10736
10737 if (expandoButton.classList.contains('commentImg')) {
10738 insertAfter(expandoButton, imgDiv);
10739 } else {
10740 expandoButton.parentNode.appendChild(imgDiv);
10741 }
10742 expandoButton.expandoBox = imgDiv;
10743
10744 expandoButton.classList.remove('collapsedExpando');
10745 expandoButton.classList.add('expanded');
10746 },
10747 /**
10748 * Recursively loads the images synchronously.
10749 */
10750 preloadImages: function(srcs, i) {
10751 var _this = this,
10752 _i = i,
10753 img = new Image();
10754 img.onload = img.onerror = function(){
10755 _i++;
10756 if(typeof srcs[_i] === 'undefined'){
10757 return;
10758 }
10759 _this.preloadImages(srcs, _i);
10760
10761 delete img; // Delete the image element from the DOM to stop the RAM usage getting to high.
10762 }
10763 img.src = srcs[i].src;
10764 },
10765 generateTextExpando: function(expandoButton) {
10766 var imageLink = expandoButton.imageLink;
10767 var wrapperDiv = document.createElement('div');
10768 wrapperDiv.className = 'usertext';
10769
10770 var imgDiv = document.createElement('div');
10771 imgDiv.className = 'madeVisible usertext-body';
10772
10773 var header = document.createElement('h3');
10774 header.className = 'imgTitle';
10775 $(header).safeHtml(imageLink.imageTitle);
10776 imgDiv.appendChild(header);
10777
10778 var text = document.createElement('div');
10779 text.className = 'md';
10780 $(text).safeHtml(imageLink.src);
10781 imgDiv.appendChild(text);
10782
10783 var captions = document.createElement('div');
10784 captions.className = 'imgCaptions';
10785 $(captions).safeHtml(imageLink.caption);
10786 imgDiv.appendChild(captions);
10787
10788 if ('credits' in imageLink) {
10789 var credits = document.createElement('div');
10790 credits.className = 'imgCredits';
10791 $(credits).safeHtml(imageLink.credits);
10792 imgDiv.appendChild(credits);
10793 }
10794
10795 wrapperDiv.appendChild(imgDiv);
10796 if (expandoButton.classList.contains('commentImg')) {
10797 insertAfter(expandoButton, wrapperDiv);
10798 } else {
10799 expandoButton.parentNode.appendChild(wrapperDiv);
10800 }
10801 expandoButton.expandoBox = imgDiv;
10802
10803 expandoButton.classList.remove('collapsedExpando');
10804 expandoButton.classList.remove('collapsed');
10805 expandoButton.classList.add('expanded');
10806
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.
10809 },
10810 generateVideoExpando: function(expandoButton, options) {
10811 var imageLink = expandoButton.imageLink;
10812 var wrapperDiv = document.createElement('div');
10813 wrapperDiv.className = 'usertext';
10814
10815 var imgDiv = document.createElement('div');
10816 imgDiv.className = 'madeVisible usertext-body';
10817
10818 var header = document.createElement('h3');
10819 header.className = 'imgTitle';
10820 $(header).safeHtml(imageLink.imageTitle);
10821 imgDiv.appendChild(header);
10822
10823 var video = document.createElement('video');
10824 video.addEventListener('click', modules['showImages'].handleVideoClick);
10825 video.setAttribute('controls','');
10826 video.setAttribute('preload','');
10827 if (options) {
10828 if (options.autoplay) {
10829 video.setAttribute('autoplay','');
10830 }
10831 if (options.muted) {
10832 video.setAttribute('muted','');
10833 }
10834 if (options.loop) {
10835 video.setAttribute('loop','');
10836 }
10837 }
10838 var sourcesHTML = "",
10839 sources = $(imageLink).data('sources'),
10840 source, sourceEle;
10841
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);
10848 }
10849
10850 imgDiv.appendChild(video);
10851
10852 if ('credits' in imageLink) {
10853 var credits = document.createElement('div');
10854 credits.className = 'imgCredits';
10855 $(credits).safeHtml(imageLink.credits);
10856 imgDiv.appendChild(credits);
10857 }
10858
10859 wrapperDiv.appendChild(imgDiv);
10860 if (expandoButton.classList.contains('commentImg')) {
10861 insertAfter(expandoButton, wrapperDiv);
10862 } else {
10863 expandoButton.parentNode.appendChild(wrapperDiv);
10864 }
10865 expandoButton.expandoBox = imgDiv;
10866
10867 expandoButton.classList.remove('collapsedExpando');
10868 expandoButton.classList.remove('collapsed');
10869 expandoButton.classList.add('expanded');
10870
10871 modules['showImages'].trackImageLoad(imageLink, video);
10872
10873 },
10874 generateAudioExpando: function(expandoButton) {
10875 var imageLink = expandoButton.imageLink;
10876 var wrapperDiv = document.createElement('div');
10877 wrapperDiv.className = 'usertext';
10878
10879 var imgDiv = document.createElement('div');
10880 imgDiv.className = 'madeVisible usertext-body';
10881
10882 var header = document.createElement('h3');
10883 header.className = 'imgTitle';
10884 $(header).safeHtml(imageLink.imageTitle);
10885 imgDiv.appendChild(header);
10886
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.
10891
10892 var sourcesHTML = "",
10893 sources = $(imageLink).data('sources'),
10894 source, sourceEle;
10895
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);
10902 }
10903
10904 imgDiv.appendChild(audio);
10905
10906 if ('credits' in imageLink) {
10907 var credits = document.createElement('div');
10908 credits.className = 'imgCredits';
10909 $(credits).safeHtml(imageLink.credits);
10910 imgDiv.appendChild(credits);
10911 }
10912
10913 wrapperDiv.appendChild(imgDiv);
10914 if (expandoButton.classList.contains('commentImg')) {
10915 insertAfter(expandoButton, wrapperDiv);
10916 } else {
10917 expandoButton.parentNode.appendChild(wrapperDiv);
10918 }
10919 expandoButton.expandoBox = imgDiv;
10920
10921 expandoButton.classList.remove('collapsedExpando');
10922 expandoButton.classList.remove('collapsed');
10923 expandoButton.classList.add('expanded');
10924
10925 modules['showImages'].trackImageLoad(imageLink, audio);
10926 },
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.
10934 //
10935 // if (e.target.paused) {
10936 // e.target.play();
10937 // }
10938 // else {
10939 // e.target.pause();
10940 // }
10941 },
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();
10947
10948 GM_xmlhttpRequest({
10949 method: 'GET',
10950 url: apiURL,
10951 // aggressiveCache: true,
10952 onload: function(response) {
10953 try {
10954 var json = JSON.parse(response.responseText);
10955 siteMod.calls[apiURL] = json;
10956 modules['showImages'].handleNoEmbedQuery(expandoButton, json);
10957 def.resolve(elem, json);
10958 } catch (error) {
10959 siteMod.calls[apiURL] = null;
10960 def.reject();
10961 }
10962 },
10963 onerror: function(response) {
10964 def.reject();
10965 }
10966 });
10967 },
10968 handleNoEmbedQuery: function(expandoButton, response) {
10969 var imageLink = expandoButton.imageLink;
10970
10971 var wrapperDiv = document.createElement('div');
10972 wrapperDiv.className = 'usertext';
10973
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);
10979 }
10980 if (imageLink.siteMod.height) {
10981 noEmbedFrame.setAttribute('height', imageLink.siteMod.height);
10982 }
10983 if (imageLink.siteMod.urlMod) {
10984 noEmbedFrame.setAttribute('src', imageLink.siteMod.urlMod(response.url));
10985 }
10986 for (key in response) {
10987 switch(key) {
10988 case 'url':
10989 if (!noEmbedFrame.hasAttribute('src')) {
10990 noEmbedFrame.setAttribute('src', response[key]);
10991 }
10992 break;
10993 case 'width':
10994 noEmbedFrame.setAttribute('width', response[key]);
10995 break;
10996 case 'height':
10997 noEmbedFrame.setAttribute('height', response[key]);
10998 break;
10999 }
11000 }
11001 noEmbedFrame.className = 'madeVisible usertext-body';
11002
11003 wrapperDiv.appendChild(noEmbedFrame);
11004 if (expandoButton.classList.contains('commentImg')) {
11005 insertAfter(expandoButton, wrapperDiv);
11006 } else {
11007 expandoButton.parentNode.appendChild(wrapperDiv);
11008 }
11009
11010 expandoButton.expandoBox = noEmbedFrame;
11011
11012 expandoButton.classList.remove('collapsedExpando');
11013 expandoButton.classList.remove('collapsed');
11014 expandoButton.classList.add('expanded');
11015
11016 modules['showImages'].trackImageLoad(imageLink, video);
11017 },
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;
11022
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);
11029 }
11030 } else {
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);
11037 }
11038 }, false);
11039 }
11040 }
11041
11042 image.addEventListener('load', function(e) {
11043 modules['showImages'].handleSRStyleToggleVisibility(e.target);
11044 }, false);
11045 },
11046 imageTrackShift: function() {
11047 var url = modules['showImages'].imageTrackStack.shift();
11048 if (typeof url === 'undefined') {
11049 modules['showImages'].handleSRStyleToggleVisibility();
11050 return;
11051 }
11052 if (BrowserDetect.isChrome()) {
11053 if (!chrome.extension.inIncognitoContext) {
11054 chrome.extension.sendMessage({
11055 requestType: 'addURLToHistory',
11056 url: url
11057 });
11058 }
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!
11062 var thisJSON = {
11063 requestType: 'addURLToHistory',
11064 url: url
11065 }
11066 self.postMessage(thisJSON);
11067 modules['showImages'].imageTrackShift();
11068 } else if (BrowserDetect.isOpera()) {
11069 var thisJSON = {
11070 requestType: 'addURLToHistory',
11071 url: url
11072 }
11073 opera.extension.postMessage(JSON.stringify(thisJSON));
11074 } else if (BrowserDetect.isSafari()) {
11075 var thisJSON = {
11076 requestType: 'addURLToHistory',
11077 url: url
11078 }
11079 safari.self.tab.dispatchMessage('addURLToHistory', thisJSON);
11080 } else if (typeof modules['showImages'].imageTrackFrame.contentWindow !== 'undefined') {
11081 modules['showImages'].imageTrackFrame.contentWindow.location.replace(url);
11082 } else {
11083 modules['showImages'].imageTrackFrame.location.replace(url);
11084 }
11085 },
11086 dragTargetData: {
11087 //numbers just picked as sane initialization values
11088 imageWidth: 100,
11089 diagonal: 0, //zero to represent the state where no the mouse button is not down
11090 dragging: false
11091 },
11092 getDragSize: function(e){
11093 var rc = e.target.getBoundingClientRect(),
11094 p = Math.pow,
11095 dragSize = p(p(e.clientX-rc.left, 2)+p(e.clientY-rc.top, 2), .5);
11096
11097 return Math.round(dragSize);
11098 },
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');
11104
11105 for (var i = 0 ; i < imageElems.length; i++) {
11106 var imageEle = imageElems[i];
11107 var imageID = imageEle.getAttribute('id');
11108
11109 if (RESUtils.doElementsCollide(toggleEle, imageEle, 15)) {
11110 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'imageZoom-' + imageID);
11111 } else {
11112 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'imageZoom-' + imageID);
11113 }
11114 }
11115 });
11116 },
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'));
11124 }
11125 $(imageTag).load(modules['showImages'].syncPlaceholder);
11126 },
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');
11133 },
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);
11141 }
11142 },
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();
11151 }
11152 },
11153 mouseoutImage: function(e) {
11154 modules['showImages'].dragTargetData.diagonal = 0;
11155 },
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);
11162
11163 modules['showImages'].resizeImage(e.target, maxWidth);
11164 modules['showImages'].dragTargetData.dragging = true;
11165 }
11166 modules['showImages'].handleSRStyleToggleVisibility(e.target);
11167 if (e.type === 'mouseup') {
11168 modules['showImages'].dragTargetData.diagonal = 0;
11169 }
11170 },
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();
11176 return false;
11177 }
11178 modules['showImages'].dragTargetData.hasChangedWidth = false;
11179 },
11180 resizeImage: function(image, newWidth) {
11181 var currWidth = $(image).width();
11182 if (newWidth !== currWidth) {
11183 modules['showImages'].dragTargetData.hasChangedWidth = true;
11184
11185 image.style.width=newWidth + 'px';
11186 image.style.maxWidth=newWidth + 'px';
11187 image.style.maxHeight='';
11188 image.style.height='auto';
11189
11190 var thisPH = $(image).data('imagePlaceholder');
11191 $(thisPH).width($(image).width() + 'px');
11192 $(thisPH).height($(image).height() + 'px');
11193 }
11194 },
11195 siteModules: {
11196 'default': {
11197 acceptRegex: /^[^#]+?\.(gif|jpe?g|png)(?:[?&#_].*|$)/i,
11198 rejectRegex: /(wikipedia\.org\/wiki|photobucket\.com|gifsound\.com|\/wiki\/File:.*)/i,
11199 go: function(){},
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));
11204 },
11205 handleLink: function(elem) {
11206 var def = $.Deferred();
11207 var href = elem.href;
11208
11209 def.resolve(elem, {
11210 type: 'IMAGE',
11211 src: elem.href
11212 });
11213 return def.promise()
11214 },
11215 handleInfo: function(elem, info) {
11216 var def = $.Deferred();
11217
11218 elem.type = info.type;
11219 elem.src = info.src;
11220 elem.href = info.src;
11221
11222 if (RESUtils.pageType() === 'linklist') {
11223 $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
11224 }
11225
11226 def.resolve(elem);
11227 return def.promise();
11228 }
11229 },
11230 imgur: {
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/',
11240 calls: {},
11241 go: function(){},
11242 detect: function(elem) {
11243 return elem.href.toLowerCase().indexOf('imgur.com/') !== -1;
11244 },
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) {
11256 return {
11257 image: {title: '', caption: '', hash: hash},
11258 links: {original: 'http://i.imgur.com/'+hash+'.jpg'}
11259 };
11260 })}
11261 });
11262 } else {
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: {
11266 links: {
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'
11269 }, image: {}}
11270 });
11271 }
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]);
11278 } else {
11279 def.reject();
11280 }
11281 } else {
11282 GM_xmlhttpRequest({
11283 method: 'GET',
11284 url: apiURL,
11285 // aggressiveCache: true,
11286 onload: function(response) {
11287 try {
11288 var json = JSON.parse(response.responseText);
11289 siteMod.calls[apiURL] = json;
11290 def.resolve(elem, json);
11291 } catch (error) {
11292 siteMod.calls[apiURL] = null;
11293 def.reject();
11294 }
11295 },
11296 onerror: function(response) {
11297 def.reject();
11298 }
11299 });
11300 }
11301 } else {
11302 def.reject();
11303 }
11304 return def.promise();
11305 },
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)
11314 info = {
11315 image: {
11316 links: {
11317 original: 'http://i.imgur.com/' + elem.imgHash + '.jpg'
11318 },
11319 image: {}
11320 }
11321 };
11322 return modules['showImages'].siteModules['imgur'].handleSingleImage(elem, info);
11323 } else {
11324 return $.Deferred().reject().promise();
11325 // console.log("ERROR", info);
11326 // console.log(arguments.callee.caller);
11327 }
11328 },
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);
11334 }
11335 elem.type = 'IMAGE';
11336 if (info.image.image.caption) elem.caption = info.image.image.caption;
11337 return $.Deferred().resolve(elem).promise();
11338 },
11339 handleGallery: function(elem, info) {
11340 var base = elem.href.split('#')[0];
11341 elem.src = info.album.images.map(function(e, i, a) {
11342 return {
11343 title: e.image.title,
11344 src: e.links.original,
11345 href: base + '#' + e.image.hash,
11346 caption: e.image.caption
11347 };
11348 });
11349 if (elem.hash) {
11350 var hash = elem.hash.slice(1);
11351 if (isNaN(hash)) {
11352 for (var i = 0; i < elem.src.length; i++) {
11353 if (hash == info.album.images[i].image.hash) {
11354 elem.galleryStart = i;
11355 break;
11356 }
11357 }
11358 } else {
11359 elem.galleryStart = parseInt(hash, 10);
11360 }
11361 }
11362 elem.imageTitle = info.album.title;
11363 elem.caption = info.album.description;
11364 elem.type = 'GALLERY';
11365 return $.Deferred().resolve(elem).promise();
11366 }
11367 },
11368 gfycat: {
11369 calls: {},
11370 go: function() {
11371 },
11372 detect: function(elem) {
11373 var href = elem.href.toLowerCase();
11374 return href.indexOf('gfycat.com') !== -1 && href.substring(-1) !== '+';
11375 },
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);
11380
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)
11387 hotLink = true;
11388
11389 var siteMod = modules['showImages'].siteModules['gfycat'];
11390 var apiURL = 'http://gfycat.com/cajax/get/' + groups[1];
11391
11392 if (apiURL in siteMod.calls) {
11393 if (siteMod.calls[apiURL] != null) {
11394 def.resolve(elem, siteMod.calls[apiURL]);
11395 } else {
11396 siteMod.calls[apiURL] = null;
11397 def.reject();
11398 }
11399 } else {
11400 GM_xmlhttpRequest({
11401 method: 'GET',
11402 url: apiURL,
11403 aggressiveCache: true,
11404 onload: function(response) {
11405 try {
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);
11411 } catch(error) {
11412 siteMod.calls[apiURL] = null;
11413 def.reject()
11414 }
11415 },
11416 onerror: function(response) {
11417 def.reject();
11418 }
11419 });
11420 }
11421 return def.promise();
11422 },
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;
11428 }
11429 return Math.max(bytes, 0.1).toFixed(1) + byteUnits[i];
11430 }
11431 if(info.hotLink) {
11432 elem.type="IMAGE";
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>)";
11439
11440 if (RESUtils.pageType() === 'linklist') {
11441 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11442 }
11443 return $.Deferred().resolve(elem).promise();
11444 }
11445
11446 elem.mediaOptions = {
11447 autoplay: true,
11448 loop: true
11449 }
11450
11451 sources = [];
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);
11456
11457 if (RESUtils.pageType() === 'linklist') {
11458 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11459 }
11460
11461 return $.Deferred().resolve(elem).promise();
11462 }
11463 },
11464 ehost: {
11465 go: function() {},
11466 detect: function(elem) {
11467 var href = elem.href.toLowerCase();
11468 return href.indexOf('eho.st') !== -1 && href.substring(-1) !== '+';
11469 },
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);
11474 if (groups) {
11475 def.resolve(elem, {
11476 src: 'http://i.eho.st/'+groups[1]+'.jpg'
11477 });
11478 } else {
11479 def.reject();
11480 }
11481 return def.promise();
11482 },
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);
11489 }
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';
11495 }
11496 }
11497 return $.Deferred().resolve(elem).promise();
11498 }
11499 },
11500 picsarus: {
11501 go: function() {},
11502 detect: function(elem) {
11503 var href = elem.href.toLowerCase();
11504 return href.indexOf('picsarus.com') !== -1 && href.substring(-1) !== '+';
11505 },
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);
11510 if (groups) {
11511 def.resolve(elem, {
11512 src: 'http://www.picsarus.com/'+groups[1]+'.jpg'
11513 });
11514 } else {
11515 def.reject();
11516 }
11517 return def.promise();
11518 },
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);
11525 }
11526 return $.Deferred().resolve(elem).promise();
11527 }
11528 },
11529 snaggy: {
11530 go: function() {},
11531 detect: function(elem) {
11532 return elem.href.toLowerCase().indexOf('snag.gy/') !== -1;
11533 },
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();
11542 },
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);
11549 }
11550 return $.Deferred().resolve(elem).promise();
11551 }
11552 },
11553 picshd: {
11554 go: function() {},
11555 detect: function(elem) {
11556 var href = elem.href.toLowerCase();
11557 return href.indexOf('picshd.com/') !== -1;
11558 },
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);
11563 if (groups) {
11564 def.resolve(elem, 'http://i.picshd.com/'+groups[1]+'.jpg');
11565 } else {
11566 def.reject();
11567 }
11568 return def.promise();
11569 },
11570 handleInfo: function(elem, info) {
11571 elem.type = 'IMAGE';
11572 elem.src = info;
11573 elem.href = info;
11574 if (RESUtils.pageType() === 'linklist') {
11575 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11576 }
11577 return $.Deferred().resolve(elem).promise();
11578 }
11579 },
11580 minus: {
11581 calls: {},
11582 go: function() {},
11583 detect: function(elem) {
11584 var href = elem.href.toLowerCase();
11585 return href.indexOf('min.us') !== -1 && href.indexOf('blog.') === -1;
11586 },
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]);
11604 } else {
11605 def.reject();
11606 }
11607 } else {
11608 GM_xmlhttpRequest({
11609 method: 'GET',
11610 url: apiURL,
11611 onload: function(response) {
11612 try {
11613 var json = JSON.parse(response.responseText);
11614 modules['showImages'].siteModules['minus'].calls[apiURL] = json;
11615 def.resolve(elem, json);
11616 } catch (e) {
11617 modules['showImages'].siteModules['minus'].calls[apiURL] = null;
11618 def.reject();
11619 }
11620 },
11621 onerror: function(response) {
11622 def.reject();
11623 }
11624 });
11625 }
11626 } else { // if not 'm', not a gallery, we can't do anything with the API.
11627 def.reject();
11628 }
11629 } else {
11630 def.reject();
11631 }
11632 } else {
11633 def.reject();
11634 }
11635 return def.promise();
11636 },
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';
11644 elem.src = {
11645 src: info.ITEMS_GALLERY
11646 };
11647 } else {
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);
11652 }
11653 elem.src = info.ITEMS_GALLERY[0];
11654 }
11655 def.resolve(elem);
11656 } else {
11657 def.reject();
11658 }
11659 return def.promise()
11660 }
11661 },
11662 flickr: {
11663 go: function() {},
11664 detect: function(elem) {
11665 var hashRe = /^http:\/\/(?:\w+)\.?flickr\.com\/(?:.*)\/([\d]{10})\/?(?:.*)?$/i;
11666 var href = elem.href;
11667 return hashRe.test(href);
11668 },
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);
11680 }
11681
11682 href += '/sizes/c' + inFragment;
11683 }
11684 href = href.replace('/lightbox', '');
11685 href = 'http://www.flickr.com/services/oembed/?format=json&url=' + href;
11686 GM_xmlhttpRequest({
11687 method: 'GET',
11688 url: href,
11689 onload: function(response) {
11690 try {
11691 var json = JSON.parse(response.responseText);
11692 def.resolve(elem, json);
11693 } catch (e) {
11694 def.reject();
11695 }
11696 },
11697 onerror: function(response) {
11698 def.reject();
11699 }
11700 })
11701 return def.promise();
11702 },
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;
11712 } else {
11713 elem.src = info.thumbnail_url;
11714 // elem.href = info.thumbnail_url;
11715 }
11716 if (RESUtils.pageType() === 'linklist') {
11717 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11718 }
11719 elem.credits = 'Picture by: <a href="'+info.author_url+'">'+info.author_name+'</a> @ Flickr';
11720 elem.type = 'IMAGE';
11721 def.resolve(elem);
11722 } else {
11723 def.reject()
11724 }
11725 return def.promise();
11726 }
11727 },
11728 steam: {
11729 go: function() {},
11730 detect: function(elem) {
11731 return elem.href.toLowerCase().indexOf('cloud.steampowered.com') !== -1;
11732 },
11733 handleLink: function(elem) {
11734 return $.Deferred().resolve(elem, elem.href).promise();
11735 },
11736 handleInfo: function(elem, info) {
11737 elem.type = 'IMAGE';
11738 elem.src = info;
11739 elem.href = info;
11740 if (RESUtils.pageType() === 'linklist') {
11741 $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
11742 }
11743 return $.Deferred().resolve(elem).promise();
11744 }
11745 },
11746 deviantart: {
11747 calls: {},
11748 matchRe: /^http:\/\/(?:fav\.me\/.*|(?:.+\.)?deviantart\.com\/(?:art\/.*|[^#]*#\/d.*))$/i,
11749 go: function() {},
11750 detect: function(elem) {
11751 return modules['showImages'].siteModules['deviantart'].matchRe.test(elem.href);
11752 },
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]);
11760 } else {
11761 def.reject();
11762 }
11763 } else {
11764 GM_xmlhttpRequest({
11765 method: 'GET',
11766 url: apiURL,
11767 // aggressiveCache: true,
11768 onload: function(response) {
11769 try {
11770 var json = JSON.parse(response.responseText);
11771 siteMod.calls[apiURL] = json;
11772 def.resolve(elem, json);
11773 } catch(error) {
11774 siteMod.calls[apiURL] = null;
11775 def.reject()
11776 }
11777 },
11778 onerror: function(response) {
11779 def.reject();
11780 }
11781 });
11782 }
11783 return def.promise();
11784 },
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;
11793 } else {
11794 elem.src = info.thumbnail_url;
11795 // elem.href = info.thumbnail_url;
11796 }
11797 if (RESUtils.pageType() === 'linklist') {
11798 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11799 }
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';
11803 def.resolve(elem);
11804 } else {
11805 def.reject()
11806 }
11807 return def.promise();
11808 }
11809 },
11810 tumblr: {
11811 calls: {},
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);
11817 },
11818 handleLink: function(elem) {
11819 var def = $.Deferred();
11820 var siteMod = modules['showImages'].siteModules['tumblr'];
11821 var groups = siteMod.matchRE.exec(elem.href);
11822 if (groups) {
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]);
11827 } else {
11828 def.reject();
11829 }
11830 } else {
11831 GM_xmlhttpRequest({
11832 method:'GET',
11833 url: apiURL,
11834 // aggressiveCache: true,
11835 onload: function(response) {
11836 try {
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);
11841 } else {
11842 siteMod.calls[apiURL] = null;
11843 def.reject();
11844 }
11845 } catch (error) {
11846 siteMod.calls[apiURL] = null;
11847 def.reject();
11848 }
11849 },
11850 onerror: function(response) {
11851 def.reject();
11852 }
11853 });
11854 }
11855 } else {
11856 def.reject();
11857 }
11858 return def.promise();
11859 },
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) {
11865 case 'photo':
11866 if (post.photos.length > 1) {
11867 elem.type = 'GALLERY';
11868 elem.src = post.photos.map(function(e) {
11869 return {
11870 src: e.original_size.url,
11871 caption: e.caption
11872 };
11873 });
11874 } else {
11875 elem.type = "IMAGE";
11876 elem.src = post.photos[0].original_size.url;
11877 }
11878 break;
11879 case 'text':
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;
11886 }
11887 break;
11888 default:
11889 return def.reject().promise();
11890 break;
11891 }
11892 elem.caption = post.caption;
11893 if (RESUtils.pageType() === 'linklist') {
11894 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11895 }
11896 elem.credits = 'Posted by: <a href="'+info.response.blog.url+'">'+info.response.blog.name+'</a> @ Tumblr';
11897 def.resolve(elem);
11898 return def.promise()
11899 }
11900 },
11901 memecrunch: {
11902 go: function() {},
11903 detect: function(elem) {
11904 return elem.href.toLowerCase().indexOf('memecrunch.com') !== -1;
11905 },
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');
11912 } else {
11913 def.reject();
11914 }
11915 return def.promise();
11916 },
11917 handleInfo: function(elem, info) {
11918 elem.type = 'IMAGE';
11919 elem.src = info;
11920 elem.href = info;
11921 if (RESUtils.pageType() === 'linklist') {
11922 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
11923 }
11924 modules['showImages'].createImageExpando(elem);
11925 }
11926 },
11927 mediacrush: {
11928 calls: {},
11929 go: function() {},
11930 detect: function(elem) {
11931 return elem.href.toLowerCase().indexOf('mediacru.sh') !== -1;
11932 },
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]);
11943 } else {
11944 siteMod.calls[apiURL] = null;
11945 def.reject();
11946 }
11947 } else {
11948 GM_xmlhttpRequest({
11949 method: 'GET',
11950 url: apiURL,
11951 // aggressiveCache: true,
11952 onload: function(response) {
11953 try {
11954 var json = JSON.parse(response.responseText);
11955 siteMod.calls[apiURL] = json;
11956 def.resolve(elem, json);
11957 } catch(error) {
11958 siteMod.calls[apiURL] = null;
11959 def.reject()
11960 }
11961 },
11962 onerror: function(response) {
11963 def.reject();
11964 }
11965 });
11966 }
11967 return def.promise();
11968 },
11969 handleInfo: function(elem, info) {
11970 var def = $.Deferred()
11971 // check files to see if video or image
11972 elem.type = 'IMAGE';
11973 var mediaData = {
11974 src: 'http://mediacru.sh/'+info.original,
11975 sources: []
11976 }
11977 if (info.original.indexOf('.gif') !== -1) {
11978 elem.mediaOptions = {
11979 autoplay: true,
11980 muted: true,
11981 loop: true
11982 }
11983 }
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]);
11989 }
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]);
11994 }
11995 }
11996 if (elem.type === 'IMAGE') {
11997 elem.src = mediaData.src;
11998 }
11999 $(elem).data('sources', mediaData.sources);
12000 return $.Deferred().resolve(elem).promise();
12001 }
12002 },
12003 vine: {
12004 calls: {},
12005 width: '480px',
12006 height: '530px',
12007 urlMod: function(url) {
12008 return url+'/embed/postcard';
12009 },
12010 go: function() {},
12011 detect: function(elem) {
12012 return elem.href.toLowerCase().indexOf('vine.co') !== -1;
12013 },
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);
12018 if (groups) {
12019 def.resolve(elem, {
12020 src: elem.href
12021 });
12022 } else {
12023 def.reject();
12024 }
12025 return def.promise();
12026 },
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();
12033 }
12034 },
12035 livememe: {
12036 go: function() { },
12037 detect: function(elem) {
12038 return elem.href.toLowerCase().indexOf('livememe.com') !== -1;
12039 },
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);
12044 if (groups) {
12045 def.resolve(elem, 'http://www.livememe.com/'+groups[1]+'.jpg');
12046 } else {
12047 def.reject();
12048 }
12049 return def.promise();
12050 },
12051 handleInfo: function(elem, info) {
12052 elem.type = 'IMAGE';
12053 elem.src = info;
12054 elem.href = info;
12055 if (RESUtils.pageType() === 'linklist') {
12056 $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
12057 }
12058 return $.Deferred().resolve(elem).promise();
12059 }
12060 },
12061 makeameme: {
12062 go: function() {},
12063 detect: function(elem) {
12064 return elem.href.toLowerCase().indexOf('makeameme.org') !== -1;
12065 },
12066 handleLink: function(elem) {
12067 var def = $.Deferred()
12068 var hashRe = /^http:\/\/makeameme\.org\/meme\/([\w-]+)\/?/i;
12069 var groups = hashRe.exec(elem.href);
12070 if (groups) {
12071 def.resolve(elem, 'http://makeameme.org/media/created/'+groups[1]+'.jpg');
12072 } else {
12073 def.reject();
12074 }
12075 return def.promise();
12076 },
12077 handleInfo: function(elem, info) {
12078 elem.type = 'IMAGE';
12079 elem.src = info;
12080 elem.href = info;
12081 if (RESUtils.pageType() === 'linklist') {
12082 $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
12083 }
12084 return $.Deferred().resolve(elem).promise();
12085 }
12086 },
12087 memefive: {
12088 go: function() {},
12089 detect: function(elem) {
12090 return elem.href.toLowerCase().indexOf('memefive.com') !== -1;
12091 },
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);
12097 if (!groups) {
12098 groups = altHashRe.exec(elem.href);
12099 }
12100 if (groups) {
12101 def.resolve(elem, 'http://memefive.com/memes/'+groups[1]+'.jpg');
12102 } else {
12103 def.reject();
12104 }
12105 return def.promise();
12106 },
12107 handleInfo: function(elem, info) {
12108 elem.type = 'IMAGE';
12109 elem.src = info;
12110 elem.href = info;
12111 if (RESUtils.pageType() === 'linklist') {
12112 $(elem).closest('.thing').find('.thumbnail').attr('href', elem.href);
12113 }
12114 return $.Deferred().resolve(elem).promise();
12115 }
12116 },
12117 memegen: {
12118 go: function() { },
12119 detect: function(elem) {
12120 return elem.href.toLowerCase().indexOf('.memegen.') !== -1;
12121 },
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);
12126 if (groups) {
12127 // Animated vs static meme images.
12128 if (groups[2]) {
12129 def.resolve(elem, 'http://a.memegen.com/' + groups[3] + '.gif');
12130 } else {
12131 def.resolve(elem, 'http://m.memegen.com/' + groups[3] + '.jpg');
12132 }
12133 } else {
12134 def.reject();
12135 }
12136 return def.promise();
12137 },
12138 handleInfo: function(elem, info) {
12139 elem.type = 'IMAGE';
12140 elem.src = info;
12141 elem.href = info;
12142 if (RESUtils.pageType() === 'linklist') {
12143 $(elem).closest('.thing').find('.thumbnail').attr('href',elem.href);
12144 }
12145 return $.Deferred().resolve(elem).promise();
12146 }
12147 }
12148 }
12149 };
12150
12151 modules['showKarma'] = {
12152 moduleID: 'showKarma',
12153 moduleName: 'Show Comment Karma',
12154 category: 'Accounts',
12155 options: {
12156 separator: {
12157 type: 'text',
12158 value: '\u00b7',
12159 description: 'Separator character between post/comment karma'
12160 },
12161 useCommas: {
12162 type: 'boolean',
12163 value: false,
12164 description: 'Use commas for large karma numbers'
12165 }
12166 },
12167 description: 'Shows your comment karma next to your link karma.',
12168 isEnabled: function() {
12169 return RESConsole.getModulePrefs(this.moduleID);
12170 },
12171 include: [
12172 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
12173 ],
12174 isMatchURL: function() {
12175 return RESUtils.isMatchURL(this.moduleID);
12176 },
12177 go: function() {
12178 if ((this.isEnabled()) && (this.isMatchURL())) {
12179 if (RESUtils.loggedInUser()) {
12180 RESUtils.getUserInfo(modules['showKarma'].updateKarmaDiv);
12181 }
12182 }
12183 },
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);
12193 }
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>");
12195 }
12196 }
12197 };
12198
12199 modules['hideChildComments'] = {
12200 moduleID: 'hideChildComments',
12201 moduleName: 'Hide All Child Comments',
12202 category: 'Comments',
12203 options: {
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)
12207 // for example:
12208 automatic: {
12209 type: 'boolean',
12210 value: false,
12211 description: 'Automatically hide all but parent comments, or provide a link to hide them all?'
12212 }
12213 },
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);
12217 },
12218 include: [
12219 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
12220 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
12221 ],
12222 isMatchURL: function() {
12223 return RESUtils.isMatchURL(this.moduleID);
12224 },
12225 go: function() {
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';
12239 } else {
12240 this.setAttribute('action','hide');
12241 this.setAttribute('title','Show only replies to original poster.');
12242 this.textContent = 'hide all child comments';
12243 }
12244 }, true);
12245 toggleButton.appendChild(this.toggleAllLink);
12246 var commentMenu = document.querySelector('ul.buttons');
12247 if (commentMenu) {
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';
12264 } else {
12265 this.setAttribute('action','hide');
12266 // this.setAttribute('title','hide child comments.');
12267 this.textContent = 'hide child comments';
12268 }
12269 }, true);
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);
12275 }
12276 }
12277 if (this.options.automatic.value) {
12278 RESUtils.click(this.toggleAllLink);
12279 }
12280 }
12281 }
12282 },
12283 toggleComments: function(action, obj) {
12284 if (obj) {
12285 var thisChildren = $(obj).closest('.thing').children('.child').children('.sitetable')[0];
12286 thisChildren.style.display = (action === 'hide') ? 'none' : 'block';
12287 } else {
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'
12297 }
12298 thisToggleLink.textContent = 'show child comments';
12299 // thisToggleLink.setAttribute('title','show child comments');
12300 thisToggleLink.setAttribute('action','show');
12301 } else {
12302 if (thisChildren !== null) {
12303 thisChildren.style.display = 'block';
12304 }
12305 thisToggleLink.textContent = 'hide child comments';
12306 // thisToggleLink.setAttribute('title','hide child comments');
12307 thisToggleLink.setAttribute('action','hide');
12308 }
12309 }
12310 }
12311 }
12312 }
12313 };
12314
12315 modules['showParent'] = {
12316 moduleID: 'showParent',
12317 moduleName: 'Show Parent on Hover',
12318 category: 'Comments',
12319 options: {
12320 hoverDelay: {
12321 type: 'text',
12322 value: 500,
12323 description: 'Delay, in milliseconds, before parent hover loads. Default is 500.'
12324 },
12325 fadeDelay: {
12326 type: 'text',
12327 value: 200,
12328 description: 'Delay, in milliseconds, before parent hover fades away after the mouse leaves. Default is 200.'
12329 },
12330 fadeSpeed: {
12331 type: 'text',
12332 value: 0.3,
12333 description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
12334 },
12335 direction: {
12336 type: 'enum',
12337 value: 'down',
12338 values: [
12339 { name: 'Up', value: 'up' },
12340 { name: 'Down', value: 'down' }
12341 ],
12342 description: 'Order the parent comments to place the closest parent at the top (down) or at the bottom (up).'
12343 },
12344 },
12345 description: 'Shows the parent comments when hovering over the "parent" link of a comment.',
12346 isEnabled: function() {
12347 return RESConsole.getModulePrefs(this.moduleID);
12348 },
12349 include: [
12350 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
12351 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
12352 ],
12353 isMatchURL: function() {
12354 return RESUtils.isMatchURL(this.moduleID);
12355 },
12356 go: function() {
12357 if ((this.isEnabled()) && (this.isMatchURL())) {
12358 if (modules['showParent'].options.direction.value === 'up') {
12359 document.html.classList.add('res-parents-up');
12360 } else {
12361 document.html.classList.add('res-parents-down');
12362 }
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 , {});
12369 });
12370
12371 $('body').on('click', '#RESHoverContainer .parentCommentWrapper .arrow', modules['showParent'].handleVoteClick);
12372 }
12373 },
12374 handleVoteClick: function(evt) {
12375 var voteKey = {'-1':'disliked', 0:'unvoted', 1:'liked'};
12376
12377 var id = $(this).parent().parent().attr('data-fullname');
12378
12379 var direction = /(up|down)(mod)?/.exec(this.className);
12380 if (direction) direction = direction[1];
12381 else return;
12382
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);
12386 return;
12387 }
12388 targetButton.click();
12389
12390 var clickedDir = (direction==='up'?1:-1);
12391 var startDir;
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;
12396
12397
12398 var newDir = clickedDir===startDir ? 0 : clickedDir;
12399
12400
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);
12408 },
12409 showCommentHover: function(def, base, context) {
12410 var direction = modules['showParent'].options.direction.value;
12411 var thing = $(base);
12412
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();
12416
12417 if (direction === 'up') {
12418 parents = $(parents.get().reverse());
12419 }
12420
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) {
12427 id = id.slice(3)
12428 $(this).find('> .entry .tagline').append('<a class="bylink parentlink" href="#'+id+'"">goto comment</a>');
12429 }
12430 });
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
12443 /*
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.
12446 */
12447 parents.find('.madeVisible, .toggleImage').remove(); //image viewer
12448 parents.find('.keyNavAnnotation').remove();
12449
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);
12454 }
12455 };
12456
12457 modules['neverEndingReddit'] = {
12458 moduleID: 'neverEndingReddit',
12459 moduleName: 'Never Ending Reddit',
12460 category: 'UI',
12461 options: {
12462 // any configurable options you have go here...
12463 // options must have a type and a value..
12464 returnToPrevPage: {
12465 type: 'boolean',
12466 value: true,
12467 description: 'Return to the page you were last on when hitting "back" button?'
12468 },
12469 autoLoad: {
12470 type: 'boolean',
12471 value: true,
12472 description: 'Automatically load new page on scroll (if off, you click to load)'
12473 },
12474 notifyWhenPaused: {
12475 type: 'boolean',
12476 value: true,
12477 description: 'Show a reminder to unpause Never-Ending Reddit after pausing'
12478 },
12479 reversePauseIcon: {
12480 type: 'boolean',
12481 value: false,
12482 description: 'Show "paused" bars icon when auto-load is paused and "play" wedge icon when active'
12483 },
12484 pauseAfterEvery: {
12485 type: 'text',
12486 value: 0,
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.'
12490 },
12491 hideDupes: {
12492 type: 'enum',
12493 value: 'fade',
12494 values: [
12495 { name: 'Fade', value: 'fade' },
12496 { name: 'Hide', value: 'hide' },
12497 { name: 'Do not hide', value: 'none' }
12498 ],
12499 description: 'Fade or completely hide duplicate posts from previous pages.'
12500 }
12501 },
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);
12505 },
12506 include: [
12507 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\_\?=]*/i
12508 ],
12509 exclude: [
12510 ],
12511 isMatchURL: function() {
12512 return RESUtils.isMatchURL(this.moduleID);
12513 },
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) {
12525 case 'fade':
12526 RESUtils.addCSS('.NERdupe { opacity: 0.3; }');
12527 break;
12528 case 'hide':
12529 RESUtils.addCSS('.NERdupe { display: none; }');
12530 break;
12531 }
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; }');
12540 }
12541 },
12542 go: function() {
12543 if ((this.isEnabled()) && (this.isMatchURL())) {
12544
12545 /* if (RESUtils.pageType() !== 'linklist') {
12546 sessionStorage.NERpageURL = location.href;
12547 }
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;
12553 }
12554
12555 this.allLinks = document.body.querySelectorAll('#siteTable div.thing');
12556
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
12560
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];
12568 }
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];
12573 if (nextLink) {
12574 this.nextPageURL = nextLink.getAttribute('href');
12575 var nextXY=RESUtils.getXYpos(nextLink);
12576 this.nextPageScrollY = nextXY.y;
12577 }
12578 this.attachLoaderWidget();
12579
12580 //Reset this info if the page is in a new tab
12581 // wait, this is always tre... commenting out.
12582 /*
12583 if (window.history.length) {
12584 console.log('delete nerpage');
12585 delete sessionStorage['NERpage'];
12586 */
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;
12593 });
12594 */
12595 this.returnToPrevPageCheck(location.hash);
12596 }
12597
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);
12601 }
12602 }
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; }');
12620 } else {
12621 RESUtils.addCSS('#NREFloat { position: fixed; top: 10px; right: 10px; display: none; }');
12622 }
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');
12635 }
12636 this.setMailIcon(hasNew);
12637 } else {
12638 this.NREMail = this.navMail;
12639 RESUtils.addCSS('#NREFloat { position: fixed; top: 30px; right: 8px; display: none; }');
12640 }
12641 this.NREFloat.appendChild(this.NREPause);
12642 document.body.appendChild(this.NREFloat);
12643 }
12644 },
12645 pageMarkers: [],
12646 pageURLs: [],
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
12660 });
12661 }
12662 } else {
12663 modules['neverEndingReddit'].NREPause.classList.remove('paused');
12664 modules['neverEndingReddit'].handleScroll();
12665 }
12666 modules['neverEndingReddit'].setWidgetActionText();
12667 },
12668 returnToPrevPageCheck: function(hash) {
12669 var pageRE = /page=(\d+)/,
12670 match = pageRE.exec(hash);
12671 // Set the current page to page 1...
12672 this.currPage = 1;
12673 if (match) {
12674 var backButtonPageNumber = match[1] || 1;
12675 if (backButtonPageNumber > 1) {
12676 this.attachModalWidget();
12677 this.currPage = backButtonPageNumber;
12678 this.loadNewPage(true);
12679 }
12680 }
12681
12682 /*
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);
12688 }
12689 }
12690 sessionStorage.lastPageURL = location.href;
12691 */
12692 },
12693 handleScroll: function(e) {
12694 if (modules['neverEndingReddit'].scrollTimer) clearTimeout(modules['neverEndingReddit'].scrollTimer);
12695 modules['neverEndingReddit'].scrollTimer = setTimeout(modules['neverEndingReddit'].handleScrollAfterTimer, 300);
12696 },
12697 handleScrollAfterTimer: function(e) {
12698 var thisPageNum = 1,
12699 thisMarker;
12700
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;
12707 if (thisMarker) {
12708 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
12709 RESStorage.setItem('RESmodules.neverEndingReddit.lastPage.'+thisPageType, thisMarker.getAttribute('url'));
12710 }
12711 } else {
12712 break;
12713 }
12714 }
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;
12723 } else {
12724 if (location.hash.indexOf('page=') !== -1) {
12725 location.hash = 'page='+thisPageNum;
12726 }
12727 delete sessionStorage['NERpage'];
12728 }
12729 }
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]+/);
12735 if (thisClass) {
12736 var thisID = thisClass[0];
12737 var thisPageType = RESUtils.pageType()+'.'+RESUtils.currentSubreddit();
12738 RESStorage.setItem('RESmodules.neverEndingReddit.lastVisibleIndex.'+thisPageType, thisID);
12739 break;
12740 }
12741 }
12742 }
12743 }
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);
12748 }
12749 }
12750 if ($(window).scrollTop() > 30) {
12751 modules['neverEndingReddit'].showFloat(true);
12752 } else {
12753 modules['neverEndingReddit'].showFloat(false);
12754 }
12755 },
12756 pauseAfterPages: null,
12757 pauseAfter: function(currPageNum) {
12758 if (this.pauseAfterPages === null) {
12759 this.pauseAfterPages = parseInt(modules['neverEndingReddit'].options.pauseAfterEvery.value);
12760 }
12761
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));
12769 }
12770 },
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');
12780 } else {
12781 modules['neverEndingReddit'].dupeHash[thisCommentLink] = 1;
12782 }
12783 }
12784 return newHTML;
12785 },
12786 setMailIcon: function(newmail) {
12787 if (RESUtils.loggedInUser() === null) return false;
12788 if (newmail) {
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();
12795 } else {
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);
12802 }
12803 },
12804 attachModalWidget: function() {
12805 this.modalWidget = createElementWithID('div','NERModal');
12806 $(this.modalWidget).html('&nbsp;');
12807 this.modalContent = createElementWithID('div','NERContent');
12808 $(this.modalContent).html('<div id="NERModalClose" class="RESCloseButton">&times;</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();
12814 });
12815 },
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';
12822
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();
12827 }
12828 }, false);
12829 insertAfter(this.siteTable, this.progressIndicator);
12830 },
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'));
12836
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"
12842 }
12843
12844 $('<p />')
12845 .text(text)
12846 .appendTo(this.progressIndicator);
12847
12848 var nextpage = $('<a id="NERStaticLink">or open next page</a>')
12849 .attr('href', this.nextPageURL);
12850 $('<p />').append(nextpage)
12851 .append('&nbsp;(and clear Never-Ending stream)')
12852 .appendTo(this.progressIndicator);
12853 },
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;
12867 me.currPage = 1;
12868 me.isLoading = false;
12869 return false;
12870 }
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);
12877 } else {
12878 me.fromBackButton = false;
12879 }
12880
12881
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;
12888 return false;
12889 }
12890 GM_xmlhttpRequest({
12891 method: "GET",
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);
12896 }
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];
12911 }
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);
12918 } else {
12919 modules['neverEndingReddit'].setMailIcon(false);
12920 }
12921 // load up uppers and downers, if enabled...
12922 // maybe not necessary anymore..
12923 /*
12924 if ((modules['uppersAndDowners'].isEnabled()) && (RESUtils.pageType() === 'comments')) {
12925 modules['uppersAndDowners'].applyUppersAndDownersToComments(modules['neverEndingReddit'].nextPageURL);
12926 }
12927 */
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;
12938 }
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;
12948 if (nextLink) {
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);
12958 } else {
12959 // console.log('not over yet');
12960 var prevLink = nextPrevLinks[0];
12961 modules['neverEndingReddit'].nextPageURL = nextLink.getAttribute('href');
12962 modules['neverEndingReddit'].attachLoaderWidget();
12963 }
12964 }
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
12969 // you left off.
12970 setTimeout(modules['neverEndingReddit'].scrollToLastElement, 4000);
12971 }
12972
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();
12976 }
12977 } else {
12978 var noresults = tempDiv.querySelector('#noresults');
12979 var noresultsfound = (noresults !== null);
12980 modules['neverEndingReddit'].NERFail(noresultsfound);
12981 }
12982 var e = document.createEvent("Events");
12983 e.initEvent("neverEndingLoad", true, true);
12984 window.dispatchEvent(e);
12985 }
12986 },
12987 onerror: function(err) {
12988 modules['neverEndingReddit'].NERFail();
12989 }
12990 });
12991 } else {
12992 // console.log('load new page ignored');
12993 }
12994 },
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');
13005 }
13006 var thisXY=RESUtils.getXYpos(lastTopScrolledEle);
13007 RESUtils.scrollTo(0, thisXY.y);
13008 modules['neverEndingReddit'].fromBackButton = false;
13009 },
13010 NERFail: function(noresults) {
13011 modules['neverEndingReddit'].isLoading = false;
13012 var newHTML = createElementWithID('div','NERFail');
13013 if (noresults) {
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;');
13016 } else {
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...';
13023 }, false);
13024 }
13025 modules['neverEndingReddit'].siteTable.appendChild(newHTML);
13026 modules['neverEndingReddit'].modalWidget.style.display = 'none';
13027 modules['neverEndingReddit'].modalContent.style.display = 'none';
13028 },
13029 showFloat: function(show) {
13030 if (show) {
13031 this.NREFloat.style.display = 'block';
13032 } else {
13033 this.NREFloat.style.display = 'none';
13034 }
13035 }
13036 };
13037
13038 modules['saveComments'] = {
13039 moduleID: 'saveComments',
13040 moduleName: 'Save Comments',
13041 category: 'Comments',
13042 options: {
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)
13046 // for example:
13047 },
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);
13051 },
13052 include: [
13053 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
13054 ],
13055 exclude: [
13056 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*\/submit\/?/i,
13057 /^https?:\/\/([a-z]+)\.reddit\.com\/submit\/?/i
13058 ],
13059 isMatchURL: function() {
13060 return RESUtils.isMatchURL(this.moduleID);
13061 },
13062 beforeLoad: function() {
13063 if ((this.isEnabled()) && (this.isMatchURL())) {
13064 RESUtils.addCSS('.RES-save { cursor: help; }');
13065 }
13066 },
13067 go: function() {
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'));
13079 });
13080 $('body').on('click', 'li.unsaveComments', function(e) {
13081 // e.preventDefault();
13082 var id = this.getAttribute('unsaveID');
13083 modules['saveComments'].unsaveComment(id, this);
13084 });
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');
13092 }
13093 } else {
13094 this.addSavedCommentsTab();
13095 }
13096 // Watch for any future 'reply' forms, or stuff loaded in via "load more comments"...
13097 /*
13098 document.body.addEventListener(
13099 'DOMNodeInserted',
13100 function( event ) {
13101 if ((event.target.tagName === 'DIV') && (hasClass(event.target,'thing'))) {
13102 modules['saveComments'].addSaveLinks(event.target);
13103 }
13104 },
13105 false
13106 );
13107 */
13108 RESUtils.watchForElement('newComments', modules['saveComments'].addSaveLinks);
13109 }
13110 },
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);
13116 });
13117 },
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');
13128 } else {
13129 var nextLink = null;
13130 }
13131 } else {
13132 var nextLink = null;
13133 }
13134 } else {
13135 var nextLink = null;
13136 }
13137 var isTopLevel = ((nextLink == null) || (nextLink.indexOf('#') === -1));
13138 var userLink = commentObj.querySelector('a.author');
13139 if (userLink == null) {
13140 var saveUser = '[deleted]';
13141 } else {
13142 var saveUser = userLink.text;
13143 }
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>');
13150 } else {
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);
13156 }
13157 var whereToInsert = commentsUL.lastChild;
13158 if (isTopLevel) whereToInsert = whereToInsert.previousSibling;
13159 commentsUL.insertBefore(saveLink, whereToInsert);
13160 }
13161 },
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 = {};
13167 } else {
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];
13176 }
13177 this.storedComments = newFormat;
13178 RESStorage.setItem('RESmodules.saveComments.savedComments',JSON.stringify(newFormat));
13179 }
13180 }
13181 },
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!');
13188 } else {
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]);
13192 }
13193 var comment = obj.parentNode.parentNode.querySelector('div.usertext-body > div.md');
13194 if (comment !== null) {
13195 commentHTML = comment.innerHTML;
13196 var savedComment = {
13197 href: href,
13198 username: username,
13199 comment: commentHTML,
13200 timeSaved: Date()
13201 };
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');
13208
13209 obj.parentNode.replaceChild(unsaveObj, obj);
13210 }
13211 if (modules['keyboardNav'].isEnabled()) {
13212 modules['keyboardNav'].keyFocus(modules['keyboardNav'].keyboardLinks[modules['keyboardNav'].activeIndex]);
13213 }
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 = {}
13218 }
13219 if (typeof this.storedComments.RESPro_delete === 'undefined') {
13220 this.storedComments.RESPro_delete = {}
13221 }
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];
13226 }
13227 RESStorage.setItem('RESmodules.saveComments.savedComments', JSON.stringify(this.storedComments));
13228 if (RESUtils.proEnabled()) {
13229 modules['RESPro'].authenticate(function() {
13230 modules['RESPro'].saveModuleData('saveComments');
13231 });
13232 }
13233 }
13234 },
13235 addSavedCommentsTab: function() {
13236 var mainmenuUL = document.body.querySelector('#header-bottom-left ul.tabmenu');
13237 if (mainmenuUL) {
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');
13247 }, true);
13248 }
13249
13250 if (savedRegex.test(savedLink.href)) {
13251 $(menuItems[i]).attr('id', 'savedLinksTab');
13252 savedLink.textContent = 'saved links';
13253 }
13254 }
13255
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');
13263 });
13264 } else {
13265 $('#savedCommentsTab').click(function(e) {
13266 e.preventDefault();
13267 location.href = location.protocol + '//www.reddit.com/saved/#comments';
13268 });
13269 }
13270 }
13271 },
13272 showSavedTab: function(tab) {
13273 switch(tab) {
13274 case 'links':
13275 location.hash = 'links';
13276 this.savedLinksContent.style.display = 'block';
13277 this.savedCommentsContent.style.display = 'none';
13278 $('#savedLinksTab').addClass('selected');
13279 $('#savedCommentsTab').removeClass('selected');
13280 break;
13281 case 'comments':
13282 location.hash = 'comments';
13283 this.savedLinksContent.style.display = 'none';
13284 this.savedCommentsContent.style.display = 'block';
13285 $('#savedLinksTab').removeClass('selected');
13286 $('#savedCommentsTab').addClass('selected');
13287 break;
13288 }
13289 },
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'));
13318 }, true);
13319 this.savedCommentsContent.appendChild(thisComment);
13320 this.savedCommentsContent.appendChild(clearLeft);
13321 }
13322 }
13323 if (this.storedComments.length === 0) {
13324 $(this.savedCommentsContent).html('<li>You have not yet saved any comments.</li>');
13325 }
13326 insertAfter(this.savedLinksContent, this.savedCommentsContent);
13327 },
13328 unsaveComment: function(id, unsaveLink) {
13329 /*
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]);
13334 } else {
13335 // console.log('found match. deleted comment');
13336 }
13337 }
13338 this.storedComments = newStoredComments;
13339 */
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 = {}
13345 }
13346 if (typeof this.storedComments.RESPro_delete === 'undefined') {
13347 this.storedComments.RESPro_delete = {}
13348 }
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];
13353 }
13354 RESStorage.setItem('RESmodules.saveComments.savedComments', JSON.stringify(this.storedComments));
13355 if (RESUtils.proEnabled()) {
13356 modules['RESPro'].authenticate(function() {
13357 modules['RESPro'].saveModuleData('saveComments');
13358 });
13359 }
13360 if (typeof this.savedCommentsContent !== 'undefined') {
13361 this.savedCommentsContent.parentNode.removeChild(this.savedCommentsContent);
13362 this.drawSavedComments();
13363 this.showSavedTab('comments');
13364 } else {
13365 var commentObj = unsaveLink.parentNode.parentNode;
13366 unsaveLink.parentNode.removeChild(unsaveLink);
13367 this.addSaveLinkToComment(commentObj);
13368 }
13369 }
13370 };
13371
13372 modules['userHighlight'] = {
13373 moduleID: 'userHighlight',
13374 moduleName: 'User Highlighter',
13375 category: 'Users',
13376 description: 'Highlights certain users in comment threads: OP, Admin, Friends, Mod - contributed by MrDerk',
13377 options: {
13378 highlightOP: {
13379 type: 'boolean',
13380 value: true,
13381 description: 'Highlight OP\'s comments'
13382 },
13383 OPColor: {
13384 type: 'text',
13385 value: '#0055DF',
13386 description: 'Color to use to highlight OP. Defaults to original text color'
13387 },
13388 OPColorHover: {
13389 type: 'text',
13390 value: '#4E7EAB',
13391 description: 'Color used to highlight OP on hover.'
13392 },
13393 highlightAdmin: {
13394 type: 'boolean',
13395 value: true,
13396 description: 'Highlight Admin\'s comments'
13397 },
13398 adminColor: {
13399 type: 'text',
13400 value: '#FF0011',
13401 description: 'Color to use to highlight Admins. Defaults to original text color'
13402 },
13403 adminColorHover: {
13404 type: 'text',
13405 value: '#B3000C',
13406 description: 'Color used to highlight Admins on hover.'
13407 },
13408 highlightFriend: {
13409 type: 'boolean',
13410 value: true,
13411 description: 'Highlight Friends\' comments'
13412 },
13413 friendColor: {
13414 type: 'text',
13415 value: '#FF4500',
13416 description: 'Color to use to highlight Friends. Defaults to original text color'
13417 },
13418 friendColorHover: {
13419 type: 'text',
13420 value: '#B33000',
13421 description: 'Color used to highlight Friends on hover.'
13422 },
13423 highlightMod: {
13424 type: 'boolean',
13425 value: true,
13426 description: 'Highlight Mod\'s comments'
13427 },
13428 modColor: {
13429 type: 'text',
13430 value: '#228822',
13431 description: 'Color to use to highlight Mods. Defaults to original text color'
13432 },
13433 modColorHover: {
13434 type: 'text',
13435 value: '#134913',
13436 description: 'Color used to highlight Mods on hover. Defaults to gray.'
13437 },
13438 highlightFirstCommenter: {
13439 type: 'boolean',
13440 value: false,
13441 description: 'Highlight the person who has the first comment in a tree, within that tree'
13442 },
13443 firstCommentColor: {
13444 type: 'text',
13445 value: '#46B6CC',
13446 description: 'Color to use to highlight the first-commenter. Defaults to original text color'
13447 },
13448 firstCommentColorHover: {
13449 type: 'text',
13450 value: '#72D2E5',
13451 description: 'Color used to highlight the first-commenter on hover.'
13452 },
13453 fontColor: {
13454 type: 'text',
13455 value: 'white',
13456 description: 'Color for highlighted text.'
13457 },
13458 autoColorUsernames: {
13459 type: 'boolean',
13460 value: false,
13461 description: 'Set a unique color for each username'
13462 }
13463 },
13464 isEnabled: function() {
13465 return RESConsole.getModulePrefs(this.moduleID);
13466 },
13467 include: [
13468 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
13469 ],
13470 isMatchURL: function() {
13471 return RESUtils.isMatchURL(this.moduleID);
13472 },
13473 go: function() {
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');
13480
13481 if (this.options.autoColorUsernames.value) {
13482 RESUtils.watchForElement('newComments', this.scanPageForNewUsernames);
13483 RESUtils.watchForElement('siteTable', this.scanPageForNewUsernames);
13484 this.scanPageForNewUsernames();
13485 }
13486
13487 if (this.options.highlightFirstCommenter.value) {
13488 RESUtils.watchForElement('newComments', this.scanPageForFirstComments);
13489 this.scanPageForFirstComments();
13490 }
13491 }
13492 },
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>\
13499 </div>');
13500 $(document.body).append(dummy);
13501 this.colorTable = {
13502 'submitter': {
13503 default: RESUtils.getComputedStyle('#dummy .author.submitter', 'color'),
13504 color: this.options.OPColor.value,
13505 hoverColor: this.options.OPColorHover.value
13506 },
13507 'friend': {
13508 default: RESUtils.getComputedStyle('#dummy .author.friend', 'color'),
13509 color: this.options.friendColor.value,
13510 hoverColor: this.options.friendColorHover.value
13511 },
13512 'moderator': {
13513 default: RESUtils.getComputedStyle('#dummy .author.moderator', 'color'),
13514 color: this.options.modColor.value,
13515 hoverColor: this.options.modColorHover.value
13516 },
13517 'admin': {
13518 default: RESUtils.getComputedStyle('#dummy .author.admin', 'color'),
13519 color: this.options.adminColor.value,
13520 hoverColor: this.options.adminColorHover.value
13521 },
13522 'user': {
13523 default: '#5544CC',
13524 color: modules['userTagger'].options['highlightColor'].value,
13525 hoverColor: modules['userTagger'].options['highlightColorHover'].value,
13526 },
13527 'firstComment': {
13528 default: '#46B6CC',
13529 color: this.options.firstCommentColor.value,
13530 hoverColor: this.options.firstCommentColorHover.value
13531 }
13532 };
13533 $('#dummy').detach();
13534 },
13535 scanPageForFirstComments: function (ele) {
13536 var comments = ele
13537 ? $(ele).closest('.commentarea > .sitetable > .thing')
13538 : document.body.querySelectorAll('.commentarea > .sitetable > .thing');
13539
13540
13541 RESUtils.forEachChunked(comments, 15, 1000, function(element, i, array) {
13542 // Get identifiers
13543 var idClass;
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;
13547 }
13548
13549 if (modules['userHighlight'].firstComments[idClass]) return;
13550
13551 var author = element.querySelector('.author');
13552 if (!author) return;
13553 var authorClass;
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;
13557 }
13558
13559 var authorDidReply = element.querySelector('.child .' + authorClass);
13560 if (!authorDidReply) return;
13561
13562 modules['userHighlight'].firstComments[idClass] = true;
13563 modules['userHighlight'].doHighlight('firstComment', authorClass, '.' + idClass);
13564 });
13565 },
13566 firstComments: {},
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) {
13571 // Get identifiers
13572 var idClass;
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;
13576 }
13577
13578 if (modules['userHighlight'].coloredUsernames[idClass]) return;
13579
13580 // Choose color
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 */
13584 }
13585
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(',') + ")";
13590
13591 // Apply color
13592 modules['userHighlight'].doTextColor('.' + idClass, color);
13593 });
13594 },
13595 coloredUsernames: {},
13596 highlightUser: function (username) {
13597 var name = 'author[href$="/' + username + '"]'; // yucky, but it'll do
13598 return this.doHighlight('user', name);
13599 },
13600 doHighlight: function(name, selector, container) {
13601 if (selector == undefined) {
13602 selector = name;
13603 }
13604 if (container == undefined) {
13605 container = ''
13606 }
13607 var color, hoverColor;
13608 var color = this.colorTable[name].color;
13609 if (color === 'default') this.colorTable[name].default;
13610
13611 var hoverColor = this.colorTable[name].hoverColor;
13612 if (hoverColor === 'default') hoverColor = '#aaa';
13613 var css = '\
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; \
13620 } \
13621 ' + container + ' .collapsed .author.' + selector + ' { \
13622 color: white !important; \
13623 background-color: #AAA !important; \
13624 } \
13625 ' + container + ' .author.' + selector + ':hover {\
13626 background-color: ' + hoverColor + ' !important; \
13627 text-decoration: none !important; \
13628 }';
13629 return RESUtils.addCSS(css);
13630 },
13631 doTextColor: function (selector, color) {
13632 var css = ' \
13633 .tagline .author' + selector + ' { \
13634 color: ' + color + ' !important; \
13635 } \
13636 ';
13637 return RESUtils.addCSS(css);
13638 }
13639 };
13640
13641 modules['styleTweaks'] = {
13642 moduleID: 'styleTweaks',
13643 moduleName: 'Style Tweaks',
13644 category: 'UI',
13645 description: 'Provides a number of style tweaks to the Reddit interface',
13646 options: {
13647 navTop: {
13648 type: 'boolean',
13649 value: true,
13650 description: 'Moves the username navbar to the top (great on netbooks!)'
13651 },
13652 commentBoxes: {
13653 type: 'boolean',
13654 value: true,
13655 description: 'Highlights comment boxes for easier reading / placefinding in large threads.'
13656 },
13657 /* REMOVED for performance reasons...
13658 commentBoxShadows: {
13659 type: 'boolean',
13660 value: false,
13661 description: 'Drop shadows on comment boxes (turn off for faster performance)'
13662 },
13663 */
13664 commentRounded: {
13665 type: 'boolean',
13666 value: true,
13667 description: 'Round corners of comment boxes'
13668 },
13669 commentHoverBorder: {
13670 type: 'boolean',
13671 value: false,
13672 description: 'Highlight comment box hierarchy on hover (turn off for faster performance)'
13673 },
13674 commentIndent: {
13675 type: 'text',
13676 value: 10,
13677 description: 'Indent comments by [x] pixels (only enter the number, no \'px\')'
13678 },
13679 continuity: {
13680 type: 'boolean',
13681 value: false,
13682 description: 'Show comment continuity lines'
13683 },
13684 lightSwitch: {
13685 type: 'boolean',
13686 value: true,
13687 description: 'Enable lightswitch (toggle between light / dark reddit)'
13688 },
13689 lightOrDark: {
13690 type: 'enum',
13691 values: [
13692 { name: 'Light', value: 'light' },
13693 { name: 'Dark', value: 'dark' }
13694 ],
13695 value: 'light',
13696 description: 'Light, or dark?'
13697 },
13698 visitedStyle: {
13699 type: 'boolean',
13700 value: false,
13701 description: 'Reddit makes it so no links on comment pages appear as "visited" - including user profiles. This option undoes that.'
13702 },
13703 showExpandos: {
13704 type: 'boolean',
13705 value: true,
13706 description: 'Bring back video and text expando buttons for users with compressed link display'
13707 },
13708 hideUnvotable: {
13709 type: 'boolean',
13710 value: false,
13711 description: 'Hide vote arrows on threads where you cannot vote (e.g. archived due to age)'
13712 },
13713 colorBlindFriendly: {
13714 type: 'boolean',
13715 value: false,
13716 description: 'Use colorblind friendly styles when possible'
13717 },
13718 scrollSubredditDropdown: {
13719 type: 'boolean',
13720 value: true,
13721 description: 'Scroll the standard subreddit dropdown (useful for pinned header and disabled Subreddit Manager)'
13722 }
13723 },
13724 isEnabled: function() {
13725 return RESConsole.getModulePrefs(this.moduleID);
13726 },
13727 include: [
13728 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
13729 ],
13730 isMatchURL: function() {
13731 return RESUtils.isMatchURL(this.moduleID);
13732 },
13733 beforeLoad: function() {
13734 if ((this.isEnabled()) && (this.isMatchURL())) {
13735 if (RESUtils.currentSubreddit()) {
13736 this.curSubReddit = RESUtils.currentSubreddit().toLowerCase();
13737 }
13738
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; }');
13744
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; }');
13749 }
13750
13751 if (this.options.colorBlindFriendly.value) {
13752 document.html.classList.add('res-colorblind');
13753 }
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);
13758 }
13759
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 }");
13765 } else {
13766 RESUtils.addCSS(".comment .md p > a:visited { color:#551a8b }");
13767 }
13768 if (this.options.showExpandos.value) {
13769 RESUtils.addCSS('.compressed .expando-button { display: block !important; }');
13770 }
13771 if ((this.options.commentBoxes.value) && (RESUtils.pageType() === 'comments')) {
13772 this.commentBoxes();
13773 }
13774 if (this.options.hideUnvotable.value) {
13775 RESUtils.addCSS('.unvoted .arrow[onclick*=unvotable] { visibility: hidden }');
13776 RESUtils.addCSS('.voted .arrow[onclick*=unvotable] { cursor: normal; }');
13777 }
13778 }
13779 },
13780 go: function() {
13781 if ((this.isEnabled()) && (this.isMatchURL())) {
13782
13783 // get the head ASAP!
13784 this.head = document.getElementsByTagName("head")[0];
13785
13786 // handle night mode scenarios (check if subreddit is compatible, etc)
13787 this.handleNightModeAtStart();
13788
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));
13793 }
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');
13797 }
13798 if (this.options.navTop.value) {
13799 this.navTop();
13800 }
13801 if (this.options.lightSwitch.value) {
13802 this.lightSwitch();
13803 }
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;');
13808 }
13809 }
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; \
13816 }');
13817 }
13818 }
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);
13831 }
13832 }
13833 }
13834 this.userbarHider();
13835 this.subredditStyles();
13836 }
13837 },
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');
13843 }
13844 var idx = this.nightModeWhitelist.indexOf(this.curSubReddit);
13845 if (idx !== -1) {
13846 // go no further. this subreddit is whitelisted.
13847 return;
13848 }
13849
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);
13852
13853 // if night mode is on and the sub isn't compatible, disable its stylesheet.
13854 if (this.isDark && !this.isNightmodeCompatible) {
13855 this.disableSubredditStyle();
13856 }
13857
13858 },
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';
13865 return;
13866 }
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
13875 var thisJSON = {
13876 requestType: 'loadTweet',
13877 url: jsonURL
13878 };
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');
13885 });
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;
13889 var thisJSON = {
13890 requestType: 'loadTweet',
13891 url: jsonURL
13892 }
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;
13897 var thisJSON = {
13898 requestType: 'loadTweet',
13899 url: jsonURL
13900 }
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;
13909 var thisJSON = {
13910 requestType: 'loadTweet',
13911 url: jsonURL
13912 }
13913 self.postMessage(thisJSON);
13914 } else {
13915 GM_xmlhttpRequest({
13916 method: "GET",
13917 url: jsonURL,
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';
13923 }
13924 });
13925 }
13926 }
13927 } else {
13928 $(e.target).removeClass('expanded').addClass('collapsedExpando').addClass('collapsed');
13929 thisExpando.style.display = 'none';
13930 }
13931
13932 },
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');
13937 },
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('&raquo;');
13949 this.userbarToggle.setAttribute('title','Toggle Userbar');
13950 this.userbarToggle.classList.add('userbarHide');
13951 this.userbarToggle.addEventListener('click', function(e) {
13952 modules['styleTweaks'].toggleUserBar();
13953 }, false);
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();
13959 }
13960 }
13961 },
13962 toggleUserBar: function() {
13963 var nextEle = this.userbarToggle.nextSibling;
13964 // hide userbar.
13965 if (this.userbarToggle.classList.contains('userbarHide')) {
13966 this.userbarToggle.classList.remove('userbarHide');
13967 this.userbarToggle.classList.add('userbarShow');
13968 $(this.userbarToggle).html('&laquo;');
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;
13974 }
13975 // show userbar.
13976 } else {
13977 this.userbarToggle.classList.remove('userbarShow');
13978 this.userbarToggle.classList.add('userbarHide');
13979 $(this.userbarToggle).html('&raquo;');
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';
13984 } else {
13985 nextEle.style.display = 'inline';
13986 }
13987 nextEle = nextEle.nextSibling;
13988 }
13989 }
13990 },
13991 commentBoxes: function() {
13992 document.html.classList.add('res-commentBoxes');
13993 if (this.options.commentRounded.value) {
13994 document.html.classList.add('res-commentBoxes-rounded');
13995 }
13996 if (this.options.continuity.value) {
13997 document.html.classList.add('res-continuity');
13998 }
13999 if (this.options.commentHoverBorder.value) {
14000 document.html.classList.add('res-commentHoverBorder');
14001 }
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; }');
14005 }
14006 },
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);
14019 } else {
14020 RESUtils.setOption('styleTweaks','lightOrDark','dark');
14021 modules['styleTweaks'].lightSwitchToggle.classList.add('enabled');
14022 modules['styleTweaks'].redditDark();
14023 }
14024 }, true);
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);
14035 },
14036 subredditStyles: function() {
14037 if (! RESUtils.currentSubreddit()) return;
14038 this.ignoredSubReddits = [];
14039 var getIgnored = RESStorage.getItem('RESmodules.styleTweaks.ignoredSubredditStyles');
14040 if (getIgnored) {
14041 this.ignoredSubReddits = safeJSON.parse(getIgnored, 'RESmodules.styleTweaks.ignoredSubredditStyles');
14042 }
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);
14050
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.
14053
14054 if ((this.curSubReddit !== null) && (subredditTitle !== null)) {
14055
14056 if (this.isDark && !this.isNightmodeCompatible) {
14057 var idx = this.nightModeWhitelist.indexOf(this.curSubReddit);
14058 if (idx !== -1) {
14059 this.styleToggleCheckbox.checked = true;
14060 }
14061 } else {
14062 var idx = this.ignoredSubReddits.indexOf(this.curSubReddit);
14063 if (idx === -1) {
14064 this.styleToggleCheckbox.checked = true;
14065 } else {
14066 this.toggleSubredditStyle(false);
14067 }
14068 }
14069 this.styleToggleCheckbox.addEventListener('change', function(e) {
14070 modules['styleTweaks'].toggleSubredditStyle(this.checked);
14071 }, false);
14072 this.styleToggleContainer.appendChild(this.styleToggleCheckbox);
14073 insertAfter(subredditTitle, this.styleToggleContainer);
14074 }
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
14080 },
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;
14089
14090 if (typeof source !== "undefined") {
14091 if (visible) {
14092 self.srstyleHideLock.unlock(source);
14093 } else {
14094 self.srstyleHideLock.lock(source);
14095 }
14096 }
14097
14098 if (visible && self.srstyleHideLock.locked()) {
14099 visible = false;
14100 }
14101
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.
14105
14106 var zIndex = 'z-index: ' + (visible ? ' 2147483647' : 'auto') + ' !important;';
14107
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 );
14111 },
14112 toggleSubredditStyle: function(toggle, subreddit) {
14113 var togglesr = (subreddit) ? subreddit.toLowerCase() : this.curSubReddit;
14114 if (toggle) {
14115 this.enableSubredditStyle(subreddit);
14116 } else {
14117 this.disableSubredditStyle(subreddit);
14118 }
14119 },
14120 enableSubredditStyle: function(subreddit) {
14121 var togglesr = (subreddit) ? subreddit.toLowerCase() : this.curSubReddit;
14122
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));
14131 }
14132
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);
14138 },
14139 disableSubredditStyle: function(subreddit) {
14140 var togglesr = (subreddit) ? subreddit.toLowerCase() : this.curSubReddit;
14141
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));
14150 }
14151
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);
14156 }
14157 },
14158 redditDark: function(off) {
14159 if (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');
14165 }
14166 } else {
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');
14172 }
14173 }
14174 }
14175 };
14176
14177 modules['accountSwitcher'] = {
14178 moduleID: 'accountSwitcher',
14179 moduleName: 'Account Switcher',
14180 category: 'Accounts',
14181 options: {
14182 accounts: {
14183 type: 'table',
14184 addRowText: '+add account',
14185 fields: [
14186 { name: 'username', type: 'text' },
14187 { name: 'password', type: 'password' }
14188 ],
14189 value: [
14190 /*
14191 ['somebodymakethis','SMT','[SMT]'],
14192 ['pics','pic','[pic]']
14193 */
14194 ],
14195 description: 'Set your usernames and passwords below. They are only stored in RES preferences.'
14196 },
14197 keepLoggedIn: {
14198 type: 'boolean',
14199 value: false,
14200 description: 'Keep me logged in when I restart my browser.'
14201 },
14202 showCurrentUserName: {
14203 type: 'boolean',
14204 value: false,
14205 description: 'Show my current user name in the Account Switcher.'
14206 },
14207 dropDownStyle: {
14208 type: 'enum',
14209 values: [
14210 { name: 'snoo (alien)', value: 'alien' },
14211 { name: 'simple arrow', value: 'arrow' }
14212 ],
14213 value: 'alien',
14214 description: 'Use the "snoo" icon, or older style dropdown?'
14215 }
14216 },
14217 description: 'Store username/password pairs and switch accounts instantly while browsing Reddit!',
14218 isEnabled: function() {
14219 return RESConsole.getModulePrefs(this.moduleID);
14220 },
14221 include: [
14222 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]*/i
14223 ],
14224 isMatchURL: function() {
14225 return RESUtils.isMatchURL(this.moduleID);
14226 },
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); }');
14235 } else {
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>';
14241 }
14242 // RESUtils.addCSS('#RESAccountSwitcherIconOverlay { display: none; position: absolute; }');
14243 }
14244 },
14245 go: function() {
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>');
14255 } else {
14256 this.downArrowOverlay = $('<span id="RESAccountSwitcherIconOverlay"><span class="downArrow"></span></span>');
14257 this.downArrow = $('<span id="RESAccountSwitcherIcon"><span class="downArrow"></span></span>');
14258 }
14259 this.downArrowOverlay.on('click', function() {
14260 modules['accountSwitcher'].toggleAccountMenu(false);
14261 modules['accountSwitcher'].manageAccounts();
14262 }).appendTo(document.body);
14263
14264 this.downArrow.on('click', function() {
14265 modules['accountSwitcher'].updateUserDetails();
14266 modules['accountSwitcher'].toggleAccountMenu(true);
14267 });
14268 this.downArrowOverlay.on('mouseleave', function() {
14269 modules['accountSwitcher'].dropdownTimer = setTimeout(function() {
14270 modules['accountSwitcher'].toggleAccountMenu(false);
14271 }, 1000);
14272 });
14273
14274 // insertAfter(this.userLink, downArrow);
14275 $(this.userLink).after(this.downArrow);
14276
14277 this.accountMenu = $('<ul id="RESAccountSwitcherDropdown" class="RESDropdownList"></ul>')
14278 this.accountMenu.on('mouseenter', function() {
14279 clearTimeout(modules['accountSwitcher'].dropdownTimer);
14280 });
14281 this.accountMenu.on('mouseleave', function() {
14282 modules['accountSwitcher'].toggleAccountMenu(false);
14283 });
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){
14292 accountCount++;
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');
14297 }
14298
14299 $accountLink
14300 .data('username', username)
14301 .html(username)
14302 .css('cursor', 'pointer')
14303 .on('click', function(e) {
14304 e.preventDefault();
14305 modules['accountSwitcher'].switchTo($(this).data('username'));
14306 })
14307 .appendTo(this.accountMenu);
14308
14309 RESUtils.getUserInfo(function (userInfo) {
14310 var userDetails = username;
14311
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 + ' &middot; ' + userInfo.data.comment_karma + ')';
14315 }
14316
14317 $accountLink.html(userDetails);
14318 }, username, false);
14319 }
14320 }
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();
14328 })
14329 .appendTo(this.accountMenu);
14330 }
14331 $(document.body).append(this.accountMenu);
14332 }
14333 }
14334 },
14335 updateUserDetails: function() {
14336 this.accountMenu.find('.accountName').each(function (index) {
14337 var username = $(this).data('username'),
14338 that = this;
14339
14340 // Ignore "+ add account"
14341 if (typeof username === 'undefined') {
14342 return;
14343 }
14344
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) {
14350 return;
14351 }
14352
14353 // Display the karma of the user
14354 var userDetails = username + ' (' + userInfo.data.link_karma + ' &middot; ' + userInfo.data.comment_karma + ')';
14355 $(that).html(userDetails);
14356 }, username);
14357 }, 500 * index);
14358 });
14359 },
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;
14366 } else {
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();
14373 }
14374 }
14375 $(modules['accountSwitcher'].accountMenu).css({
14376 top: (thisY + thisHeight) + 'px',
14377 left: (thisX) + 'px'
14378 });
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'
14385 });
14386
14387 $(modules['accountSwitcher'].downArrowOverlay).show();
14388 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'accountSwitcher');
14389
14390 } else {
14391 $(modules['accountSwitcher'].accountMenu).hide();
14392 $(modules['accountSwitcher'].downArrowOverlay).hide();
14393 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'accountSwitcher');
14394 }
14395 },
14396 closeAccountMenu: function() {
14397 // this function basically just exists for other modules to call.
14398 this.accountMenu.hide();
14399 },
14400 switchTo: function(username) {
14401 var accounts = this.options.accounts.value;
14402 var password = '';
14403 var rem = '';
14404 if (this.options.keepLoggedIn.value) {
14405 rem = '&rem=on';
14406 }
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];
14411 break;
14412 }
14413 }
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';
14422 }
14423
14424 // Remove old session cookie
14425 RESUtils.deleteCookie('reddit_session');
14426
14427 GM_xmlhttpRequest({
14428 method: "POST",
14429 url: loginUrl,
14430 data: 'user='+encodeURIComponent(username)+'&passwd='+encodeURIComponent(password)+rem,
14431 headers: {
14432 "Content-Type": "application/x-www-form-urlencoded"
14433 },
14434 onload: function(response) {
14435 var badData = false;
14436 try {
14437 var data = JSON.parse(response.responseText);
14438 } catch(error) {
14439 var data = {};
14440 badData = true;
14441 }
14442
14443 var error = /WRONG_PASSWORD/;
14444 var rateLimit = /RATELIMIT/;
14445 if (badData) {
14446 RESUtils.notification({
14447 type: 'error',
14448 moduleID: 'accountSwitcher',
14449 message: 'Could not switch accounts. Reddit may be under heavy load. Please try again in a few moments.'
14450 });
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.');
14455 } else {
14456 location.reload();
14457 }
14458 }
14459 });
14460 },
14461 manageAccounts: function() {
14462 modules['settingsNavigation'].loadSettingsPage('accountSwitcher', 'accounts');
14463 }
14464 };
14465
14466 modules['filteReddit'] = {
14467 moduleID: 'filteReddit',
14468 moduleName: 'filteReddit',
14469 category: 'Filters',
14470 options: {
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)
14474 // for example:
14475 NSFWfilter: {
14476 type: 'boolean',
14477 value: false,
14478 description: 'Filters all links labelled NSFW'
14479 },
14480 notification: {
14481 type: 'boolean',
14482 value: true,
14483 description: 'Show a notification when posts are filtered'
14484 },
14485 NSFWQuickToggle: {
14486 type: 'boolean',
14487 value: true,
14488 description: 'Add a quick NSFW on/off toggle to the gear menu'
14489 },
14490 keywords: {
14491 type: 'table',
14492 addRowText: '+add filter',
14493 fields: [
14494 { name: 'keyword', type: 'text' },
14495 { name: 'applyTo',
14496 type: 'enum',
14497 values: [
14498 { name: 'Everywhere', value: 'everywhere' },
14499 { name: 'Everywhere but:', value: 'exclude' },
14500 { name: 'Only on:', value: 'include' }
14501 ],
14502 value: 'everywhere',
14503 description: 'Apply filter to:'
14504 },
14505 {
14506 name: 'reddits',
14507 type: 'list',
14508 source: '/api/search_reddit_names.json?app=res',
14509 hintText: 'type a subreddit name',
14510 onResult: function(response) {
14511 var names = response.names;
14512 var results = [];
14513 for (var i=0, len=names.length; i<len; i++) {
14514 results.push({id: names[i], name: names[i]});
14515 }
14516 return results;
14517 },
14518 onCachedResult: function(response) {
14519 var names = response.names;
14520 var results = [];
14521 for (var i=0, len=names.length; i<len; i++) {
14522 results.push({id: names[i], name: names[i]});
14523 }
14524 return results;
14525 }
14526 } //,
14527 /* { name: 'inclusions', type: 'list', source: location.protocol + '/api/search_reddit_names' } */
14528 ],
14529 value: [
14530 ],
14531 description: 'Type in title keywords you want to ignore if they show up in a title'
14532 },
14533 subreddits: {
14534 type: 'table',
14535 addRowText: '+add filter',
14536 fields: [
14537 { name: 'subreddit', type: 'text' }
14538 ],
14539 value: [
14540 ],
14541 description: 'Type in a subreddit you want to ignore (only applies to /r/all or /domain/* urls)'
14542 },
14543 domains: {
14544 type: 'table',
14545 addRowText: '+add filter',
14546 fields: [
14547 { name: 'domain', type: 'text' },
14548 { name: 'applyTo',
14549 type: 'enum',
14550 values: [
14551 { name: 'Everywhere', value: 'everywhere' },
14552 { name: 'Everywhere but:', value: 'exclude' },
14553 { name: 'Only on:', value: 'include' }
14554 ],
14555 value: 'everywhere',
14556 description: 'Apply filter to:'
14557 },
14558 {
14559 name: 'reddits',
14560 type: 'list',
14561 source: '/api/search_reddit_names.json?app=res',
14562 hintText: 'type a subreddit name',
14563 onResult: function(response) {
14564 var names = response.names;
14565 var results = [];
14566 for (var i=0, len=names.length; i<len; i++) {
14567 results.push({id: names[i], name: names[i]});
14568 }
14569 return results;
14570 },
14571 onCachedResult: function(response) {
14572 var names = response.names;
14573 var results = [];
14574 for (var i=0, len=names.length; i<len; i++) {
14575 results.push({id: names[i], name: names[i]});
14576 }
14577 return results;
14578 }
14579 } //,
14580 /* { name: 'inclusions', type: 'list', source: location.protocol + '/api/search_reddit_names' } */
14581 ],
14582 value: [
14583 ],
14584 description: 'Type in domain keywords you want to ignore. Note that \"reddit\" would ignore \"reddit.com\" and \"fooredditbar.com\"'
14585 },
14586 flair: {
14587 type: 'table',
14588 addRowText: '+add filter',
14589 fields: [
14590 { name: 'keyword', type: 'text' },
14591 { name: 'applyTo',
14592 type: 'enum',
14593 values: [
14594 { name: 'Everywhere', value: 'everywhere' },
14595 { name: 'Everywhere but:', value: 'exclude' },
14596 { name: 'Only on:', value: 'include' }
14597 ],
14598 value: 'everywhere',
14599 description: 'Apply filter to:'
14600 },
14601 {
14602 name: 'reddits',
14603 type: 'list',
14604 source: '/api/search_reddit_names.json?app=res',
14605 hintText: 'type a subreddit name',
14606 onResult: function(response) {
14607 var names = response.names;
14608 var results = [];
14609 for (var i=0, len=names.length; i<len; i++) {
14610 results.push({id: names[i], name: names[i]});
14611 }
14612 return results;
14613 },
14614 onCachedResult: function(response) {
14615 var names = response.names;
14616 var results = [];
14617 for (var i=0, len=names.length; i<len; i++) {
14618 results.push({id: names[i], name: names[i]});
14619 }
14620 return results;
14621 }
14622 } //,
14623 /* { name: 'inclusions', type: 'list', source: location.protocol + '/api/search_reddit_names' } */
14624 ],
14625 value: [
14626 ],
14627 description: 'Type in keywords you want to ignore if they are contained in link flair'
14628 },
14629 allowNSFW: {
14630 type: 'table',
14631 addRowText: "+add subreddits",
14632 description: "Whitelist subreddits from NSFW filter",
14633 fields: [
14634 {
14635 name: 'subreddits',
14636 type: 'list',
14637 source: '/api/search_reddit_names.json?app=res',
14638 hintText: 'type a subreddit name',
14639 onResult: function(response) {
14640 var names = response.names;
14641 var results = [];
14642 for (var i=0, len=names.length; i<len; i++) {
14643 results.push({id: names[i], name: names[i]});
14644 }
14645 return results;
14646 },
14647 onCachedResult: function(response) {
14648 var names = response.names;
14649 var results = [];
14650 for (var i=0, len=names.length; i<len; i++) {
14651 results.push({id: names[i], name: names[i]});
14652 }
14653 return results;
14654 }
14655 },
14656 {
14657 name: 'where',
14658 type: 'enum',
14659 values: [
14660 { name: 'Everywhere', value: 'everywhere' },
14661 { name: 'When browsing subreddit/multi-subreddit', value: 'visit' }
14662 ],
14663 value: 'everywhere'
14664 }
14665 ]
14666 }
14667 },
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);
14671 },
14672 include: [
14673 /^https?:\/\/([a-z]+)\.reddit\.com\/?(?:\??[\w]+=[\w]+&?)*/i,
14674 /^https?:\/\/([a-z]+)\.reddit\.com\/r\/[\w]+\/?(?:\??[\w]+=[\w]+&?)*$/i
14675 ],
14676 exclude: [
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
14680 ],
14681 isMatchURL: function() {
14682 return RESUtils.isMatchURL(this.moduleID);
14683 },
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();
14691 }
14692 }
14693 },
14694 go: function() {
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();
14703 }, true);
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);
14711 }
14712
14713 if ((this.isEnabled()) && (this.isMatchURL())) {
14714 this.scanEntries();
14715 RESUtils.watchForElement('siteTable', modules['filteReddit'].scanEntries);
14716 }
14717 },
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');
14723 } else {
14724 modules['filteReddit'].filterNSFW(true);
14725 RESUtils.setOption('filteReddit','NSFWfilter',true);
14726 $(modules['filteReddit'].nsfwSwitchToggle).addClass('enabled');
14727 }
14728
14729 if (notify) {
14730 var onOff = modules['filteReddit'].options.NSFWfilter.value ? 'on' : ' off';
14731
14732 RESUtils.notification({
14733 header: 'NSFW Filter',
14734 moduleID: 'filteReddit',
14735 optionKey: 'NSFWfilter',
14736 message: 'NSFW Filter has been turned ' + onOff + '.'
14737 }, 4000);
14738 }
14739 },
14740 scanEntries: function(ele) {
14741 var numFiltered = 0;
14742 var numNsfwHidden = 0;
14743
14744 var entries;
14745 if (ele == null) {
14746 entries = document.querySelectorAll('#siteTable div.thing.link');
14747 } else {
14748 entries = ele.querySelectorAll('div.thing.link');
14749 }
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;
14760 } else {
14761 var postSubreddit = false;
14762 }
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);
14769 }
14770 if ((!filtered) && (postFlair)) {
14771 filtered = modules['filteReddit'].filterFlair(postFlair.textContent, postSubreddit || RESUtils.currentSubreddit());
14772 }
14773 if (filtered) {
14774 entries[i].classList.add('RESFiltered')
14775 numFiltered++;
14776 }
14777
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) {
14782 numNsfwHidden++;
14783 }
14784 }
14785 }
14786
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.');
14792
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
14799 });
14800 }
14801 },
14802 addedNSFWFilterStyle: false,
14803 addNSFWFilterStyle: function() {
14804 if (this.addedNSFWFilterStyle) return;
14805 this.addedNSFWFilterStyle = true;
14806
14807 RESUtils.addCSS('body:not(.allowOver18) .over18 { display: none !important; }');
14808 RESUtils.addCSS('.thing.over18.allowOver18 { display: block !important; }');
14809 },
14810 filterNSFW: function(filterOn) {
14811 this.addNSFWFilterStyle();
14812 $(document.body).toggleClass('allowOver18');
14813 },
14814 filterTitle: function(title, reddit) {
14815 var reddit = (reddit) ? reddit.toLowerCase() : null;
14816 return this.arrayContainsSubstring(this.options.keywords.value, title.toLowerCase(), reddit);
14817 },
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);
14822 },
14823 filterSubreddit: function(subreddit) {
14824 return this.arrayContainsSubstring(this.options.subreddits.value, subreddit.toLowerCase(), null, true);
14825 },
14826 filterFlair: function(flair, reddit) {
14827 var reddit = (reddit) ? reddit.toLowerCase() : null;
14828 return this.arrayContainsSubstring(this.options.flair.value, flair.toLowerCase(), reddit);
14829 },
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;
14834
14835 if (typeof currSubreddit === "undefined") {
14836 currSubreddit = RESUtils.currentSubreddit();
14837 }
14838
14839 if (!this.subredditAllowNsfwOption) {
14840 this.subredditAllowNsfwOption = RESUtils.indexOptionTable('filteReddit', 'allowNSFW', 0);
14841 }
14842
14843 if (this.allowAllNsfw == null && currSubreddit) {
14844 var optionValue = this.subredditAllowNsfwOption(currSubreddit);
14845 this.allowAllNsfw = (optionValue && optionValue[1] === 'visit') || false;
14846 }
14847 if (this.allowAllNsfw) {
14848 return true;
14849 }
14850
14851 if (!postSubreddit) postSubreddit = currSubreddit;
14852 if (!postSubreddit) return false;
14853 var optionValue = this.subredditAllowNsfwOption(postSubreddit);
14854 if (optionValue) {
14855 if (optionValue[1] === 'everywhere') {
14856 return true;
14857 } else { // optionValue[1] == visit (subreddit or multisubreddit)
14858 if (RESUtils.inList(postSubreddit, currSubreddit, '+')) {
14859 return true;
14860 }
14861 }
14862 }
14863 },
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);
14869 delete temp;
14870 return result;
14871 },
14872 arrayContainsSubstring: function(obj, stringToSearch, reddit, fullmatch) {
14873 if (!obj) return false;
14874 stringToSearch = this.unescapeHTML(stringToSearch);
14875 var i = obj.length;
14876 while (i--) {
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',''];
14880 }
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));
14888 switch (applyTo) {
14889 case 'exclude':
14890 if ((applyList.indexOf(reddit) !== -1) || (checkRAll)) {
14891 skipCheck = true;
14892 }
14893 break;
14894 case 'include':
14895 if ((applyList.indexOf(reddit) === -1) && (!checkRAll)) {
14896 skipCheck = true;
14897 }
14898 break;
14899 }
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)) {
14903 return true;
14904 }
14905 }
14906 return false;
14907 },
14908 toggleFilter: function(e) {
14909 var thisSubreddit = $(e.target).data('subreddit').toLowerCase();
14910 var filteredReddits = modules['filteReddit'].options.subreddits.value || [];
14911 var exists=false;
14912 for (var i=0, len=filteredReddits.length; i<len; i++) {
14913 if ((filteredReddits[i]) && (filteredReddits[i][0].toLowerCase() === thisSubreddit)) {
14914 exists=true;
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');
14919 break;
14920 }
14921 }
14922 if (!exists) {
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');
14928 }
14929 modules['filteReddit'].options.subreddits.value = filteredReddits;
14930 // save change to options...
14931 RESStorage.setItem('RESoptions.filteReddit', JSON.stringify(modules['filteReddit'].options));
14932 }
14933 };
14934
14935 modules['newCommentCount'] = {
14936 moduleID: 'newCommentCount',
14937 moduleName: 'New Comment Count',
14938 category: 'Comments',
14939 options: {
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)
14943 // for example:
14944 cleanComments: {
14945 type: 'text',
14946 value: 7,
14947 description: 'Clean out cached comment counts of pages you haven\'t visited in [x] days - enter a number here only!'
14948 },
14949 subscriptionLength: {
14950 type: 'text',
14951 value: 2,
14952 description: 'Automatically remove thread subscriptions in [x] days - enter a number here only!'
14953 }
14954 },
14955 description: 'Shows how many new comments there are since your last visit.',
14956 isEnabled: function() {
14957 return RESConsole.getModulePrefs(this.moduleID);
14958 },
14959 include: [
14960 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
14961 ],
14962 isMatchURL: function() {
14963 return RESUtils.isMatchURL(this.moduleID);
14964 },
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; }');
14972 }
14973 },
14974 go: function() {
14975 if ((this.isEnabled()) && (this.isMatchURL())) {
14976 // go!
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();
14982
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();
14996 });
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')) {
15003 return false;
15004 }
15005 if ($(this).hasClass('active')) {
15006 $(this).toggleClass('descending');
15007 }
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'));
15013 });
15014 this.drawSubscriptionsTable();
15015 RESUtils.watchForElement('siteTable', modules['newCommentCount'].processCommentCounts);
15016 /*
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);
15020 }
15021 }, true);
15022 */
15023 } else {
15024 this.processCommentCounts();
15025 RESUtils.watchForElement('siteTable', modules['newCommentCount'].processCommentCounts);
15026 /*
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);
15030 }
15031 }, true);
15032 */
15033 }
15034 this.checkSubscriptions();
15035 }
15036 },
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);
15046 if (match) {
15047 this.commentCounts[i].subreddit = match[1].toLowerCase();
15048 thisCounts.push(this.commentCounts[i]);
15049 }
15050 }
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;
15056 });
15057 if (this.descending) thisCounts.reverse();
15058 break;
15059 case 'updateTime':
15060 thisCounts.sort(function(a,b) {
15061 return (a.updateTime > b.updateTime) ? 1 : (b.updateTime > a.updateTime) ? -1 : 0;
15062 });
15063 if (this.descending) thisCounts.reverse();
15064 break;
15065 case 'subreddit':
15066 thisCounts.sort(function(a,b) {
15067 return (a.subreddit > b.subreddit) ? 1 : (b.subreddit > a.subreddit) ? -1 : 0;
15068 });
15069 if (this.descending) thisCounts.reverse();
15070 break;
15071 default:
15072 thisCounts.sort(function(a,b) {
15073 return (a.title > b.title) ? 1 : (b.title > a.title) ? -1 : 0;
15074 });
15075 if (this.descending) thisCounts.reverse();
15076 break;
15077 }
15078 var rows = 0;
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;
15092
15093 } else {
15094 var thisExpiresContent = 'n/a';
15095 var thisActionContent = '<span class="RESSubscriptionButton subscribe" title="subscribe to this thread" data-threadid="'+thisCounts[i].id+'">subscribe</span>';
15096 }
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);
15103 rows++;
15104 }
15105 }
15106 if (rows === 0) {
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>');
15109 } else {
15110 $('#newCommentsTable tbody').append('<td colspan="5">No threads found</td>');
15111 }
15112 }
15113 },
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.'
15122 })
15123 },
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();
15129 },
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);
15135 }
15136 },
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();
15141 },
15142 subscribeButton: function(e) {
15143 var thisURL = $(e.target).attr('data-threadid');
15144 modules['newCommentCount'].subscribe(thisURL);
15145 },
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();
15151 },
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());
15159 }
15160 // Clean cache every six hours
15161 if ((now.getTime() - lastClean) > 21600000) {
15162 modules['newCommentCount'].cleanCache();
15163 }
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);
15172 if (matches) {
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;
15176 if (diff > 0) {
15177 var newString = $('<span class="newComments">&nbsp;('+diff+' new)</span>');
15178 $(commentsLinks[i]).append(newString);
15179 }
15180 }
15181 }
15182 }
15183 },
15184 updateCommentCountFromMyComment: function() {
15185 modules['newCommentCount'].updateCommentCount(true);
15186 },
15187 updateCommentCount: function(mycomment) {
15188 var thisModule = modules['newCommentCount'];
15189 var IDre = /\/r\/[\w]+\/comments\/([\w]+)\//i;
15190 var matches = IDre.exec(location.href);
15191 if (matches) {
15192 if (!thisModule.currentCommentCount) {
15193 thisModule.currentCommentID = matches[1];
15194 var thisCount = document.querySelector('#siteTable a.comments');
15195 if (thisCount) {
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">&nbsp;('+diff+' new)</span>');
15202 if (diff>0) $(thisCount).append(newString);
15203 }
15204 if (isNaN(thisModule.currentCommentCount)) thisModule.currentCommentCount = 0;
15205 if (mycomment) thisModule.currentCommentCount++;
15206 }
15207 } else {
15208 thisModule.currentCommentCount++;
15209 }
15210 }
15211 var now = new Date();
15212 if (typeof thisModule.commentCounts === 'undefined') {
15213 thisModule.commentCounts = {};
15214 }
15215 if (typeof thisModule.commentCounts[thisModule.currentCommentID] === 'undefined') {
15216 thisModule.commentCounts[thisModule.currentCommentID] = {};
15217 }
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));
15226 }, 100);
15227 // }
15228 },
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];
15237 }
15238 }
15239 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(this.commentCounts));
15240 RESStorage.setItem('RESmodules.newCommentCount.lastClean', now.getTime());
15241 },
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');
15252 } else {
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');
15256 }
15257 }
15258 },
15259 toggleSubscription: function() {
15260 var commentID = modules['newCommentCount'].currentCommentID;
15261 if (typeof modules['newCommentCount'].commentCounts[commentID].subscriptionDate !== 'undefined') {
15262 modules['newCommentCount'].unsubscribeFromThread(commentID);
15263 } else {
15264 modules['newCommentCount'].subscribeToThread(commentID);
15265 }
15266 },
15267 getLatestCommentCounts: function() {
15268 var counts = RESStorage.getItem('RESmodules.newCommentCount.counts');
15269 if (counts == null) {
15270 counts = '{}';
15271 }
15272 modules['newCommentCount'].commentCounts = safeJSON.parse(counts, 'RESmodules.newCommentCount.counts');
15273 },
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.'
15289 }, 3000);
15290 },
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.'
15304 }, 3000);
15305 },
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;
15318 }
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);
15325 }
15326 RESStorage.setItem('RESmodules.newCommentCount.count', JSON.stringify(this.commentCounts));
15327 }
15328 }
15329 if (threadsToCheck.length > 0) {
15330 this.checkThreads(threadsToCheck);
15331 }
15332 }
15333 },
15334 checkThreads: function(commentIDs) {
15335 GM_xmlhttpRequest({
15336 method: "GET",
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>'
15351 }, 10000);
15352 }
15353 }
15354 RESStorage.setItem('RESmodules.newCommentCount.counts', JSON.stringify(modules['newCommentCount'].commentCounts));
15355 }
15356 }
15357 });
15358 }
15359 };
15360
15361 modules['spamButton'] = {
15362 moduleID: 'spamButton',
15363 moduleName: 'Spam Button',
15364 category: 'Filters',
15365 options: {
15366 },
15367 description: 'Adds a Spam button to posts for easy reporting.',
15368 isEnabled: function() {
15369 return RESConsole.getModulePrefs(this.moduleID);
15370 },
15371 include: [
15372 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
15373 ],
15374 isMatchURL: function() {
15375 return RESUtils.isMatchURL(this.moduleID);
15376 },
15377 go: function() {
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');
15382 if (!reset) {
15383 RESStorage.setItem('RESmodules.spamButton.reset','true');
15384 RESConsole.enableModule('spamButton', false);
15385 }
15386
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();
15392 }
15393 }
15394 },
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++)
15400 {
15401 var permaLink = allLists[i].childNodes[0].childNodes[0].href;
15402
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);
15407
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);
15417 }
15418 }
15419 },
15420 reportPost: function(e) {
15421 var a = e.target;
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');
15428 }
15429 };
15430
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.',
15436 options: {
15437 showByDefault: {
15438 type: 'boolean',
15439 value: false,
15440 description: 'Display Comment Navigator by default'
15441 }
15442 },
15443 isEnabled: function() {
15444 return RESConsole.getModulePrefs(this.moduleID);
15445 },
15446 include: [
15447 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
15448 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
15449 ],
15450 isMatchURL: function() {
15451 return RESUtils.isMatchURL(this.moduleID);
15452 },
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; }');
15473 }
15474 },
15475 go: function() {
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');
15482 if (commentArea) {
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) {
15491 case 'submitter':
15492 thisEle.setAttribute('title','Navigate comments made by the post submitter');
15493 break;
15494 case 'moderator':
15495 thisEle.setAttribute('title','Navigate comments made by moderators');
15496 break;
15497 case 'friend':
15498 thisEle.setAttribute('title','Navigate comments made by users on your friends list');
15499 break;
15500 case 'me':
15501 thisEle.setAttribute('title','Navigate comments made by you');
15502 break;
15503 case 'admin':
15504 thisEle.setAttribute('title','Navigate comments made by reddit admins');
15505 break;
15506 case 'IAmA':
15507 thisEle.setAttribute('title','Navigate through questions that have been answered by the submitter (most useful in /r/IAmA)');
15508 break;
15509 case 'images':
15510 thisEle.setAttribute('title','Navigate through comments with images');
15511 break;
15512 case 'popular':
15513 thisEle.setAttribute('title','Navigate through comments in order of highest vote total');
15514 break;
15515 case 'new':
15516 thisEle.setAttribute('title','Navigate through new comments (Reddit Gold users only)');
15517 break;
15518 default:
15519 break;
15520 }
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');
15526 if (isGold) {
15527 thisEle.setAttribute('style','color: #9A7D2E;');
15528 } else {
15529 thisEle.classList.add('commentNavSortTypeDisabled');
15530 }
15531 }
15532 if ((thisCategory !== 'new') || (isGold)) {
15533 thisEle.addEventListener('click', function(e) {
15534 modules['commentNavigator'].showNavigator(e.target.getAttribute('index'));
15535 }, false);
15536 }
15537 this.commentNavToggle.appendChild(thisEle);
15538 if (i<len-1) {
15539 var thisDivider = document.createElement('span');
15540 thisDivider.textContent = '|';
15541 this.commentNavToggle.appendChild(thisDivider);
15542 }
15543 }
15544
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';
15549 }
15550 var navBoxHTML = ' \
15551 \
15552 <h3>Navigate by: \
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> \
15564 </select> \
15565 </h3>\
15566 <div id="commentNavCloseButton" class="RESCloseButton">&times;</div> \
15567 <div class="RESDialogContents"> \
15568 <div id="commentNavButtons"> \
15569 <div id="commentNavUp"></div> <div id="commentNavPostCount"></div> <div id="commentNavDown"></div> \
15570 </div> \
15571 </div> \
15572 ';
15573 $(this.commentNavBox).html(navBoxHTML);
15574 this.posts = [];
15575 this.nav = [];
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';
15582 }, false);
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);
15589 }
15590 }
15591 },
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';
15598 } else {
15599 modules['commentNavigator'].commentNavButtons.style.display = 'none';
15600 }
15601 },
15602 showNavigator: function(categoryID) {
15603 modules['commentNavigator'].commentNavBox.style.display = 'block';
15604 this.navSelect.selectedIndex = categoryID;
15605 modules['commentNavigator'].changeCategory();
15606 },
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) {
15612 case 'submitter':
15613 case 'moderator':
15614 case 'friend':
15615 case 'admin':
15616 this.posts[category] = document.querySelectorAll('.noncollapsed a.author.'+category);
15617 this.resetNavigator(category);
15618 break;
15619 case 'me':
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);
15624 });
15625 break;
15626 case 'IAmA':
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);
15634 } else {
15635 this.posts[category].push(sevenUp);
15636 }
15637 }
15638 this.resetNavigator(category);
15639 break;
15640 case 'images':
15641 var imagePosts = document.querySelectorAll('.toggleImage');
15642 this.posts[category] = imagePosts;
15643 this.resetNavigator(category);
15644 break;
15645 case 'popular':
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');
15650 if (thisScore) {
15651 var scoreSplit = thisScore.innerHTML.split(' ');
15652 var score = scoreSplit[0];
15653 } else {
15654 var score = 0;
15655 }
15656 commentsObj[i] = {
15657 comment: allComments[i],
15658 score: score
15659 }
15660 }
15661 commentsObj.sort(function(a, b) {
15662 return parseInt(b.score, 10) - parseInt(a.score, 10);
15663 });
15664 this.posts[category] = [];
15665 for (var i=0, len=commentsObj.length; i<len; i++) {
15666 this.posts[category][i] = commentsObj[i].comment;
15667 }
15668 this.resetNavigator(category);
15669 break;
15670 case 'new':
15671 this.posts[category] = document.querySelectorAll('.new-comment');
15672 this.resetNavigator(category);
15673 break;
15674 }
15675 }
15676 }
15677 },
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');
15685 } else {
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');
15690 }
15691 },
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]--;
15697 } else {
15698 modules['commentNavigator'].nav[category] = modules['commentNavigator'].posts[category].length - 1;
15699 }
15700 modules['commentNavigator'].scrollToNavElement();
15701 }
15702 },
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]++;
15708 } else {
15709 modules['commentNavigator'].nav[category] = 0;
15710 }
15711 modules['commentNavigator'].scrollToNavElement();
15712 }
15713 },
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);
15719 }
15720 };
15721
15722
15723 /*
15724 modules['redditProfiles'] = {
15725 moduleID: 'redditProfiles',
15726 moduleName: 'Reddit Profiles',
15727 category: 'Users',
15728 options: {
15729 },
15730 description: 'Pulls in profiles from redditgifts.com when viewing a user profile.',
15731 isEnabled: function() {
15732 return RESConsole.getModulePrefs(this.moduleID);
15733 },
15734 include: [
15735 /^http:\/\/([a-z]+).reddit.com\/user\/[-\w\.]+/i
15736 ],
15737 isMatchURL: function() {
15738 return RESUtils.isMatchURL(this.moduleID);
15739 },
15740 go: function() {
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) {
15746 thisCache = '{}';
15747 }
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);
15752 if (match) {
15753 var username = match[1];
15754 this.getProfile(username);
15755 }
15756 }
15757 },
15758 getProfile: function(username) {
15759 var lastCheck = 0;
15760 if ((typeof this.profileCache[username] !== 'undefined') && (this.profileCache[username] !== null)) {
15761 lastCheck = this.profileCache[username].lastCheck;
15762 }
15763 var now = new Date();
15764 if ((now.getTime() - lastCheck) > 900000) {
15765 var jsonURL = 'http://redditgifts.com/profiles/view-json/'+username+'/';
15766 GM_xmlhttpRequest({
15767 method: "GET",
15768 url: jsonURL,
15769 onload: function(response) {
15770 try {
15771 // if it is JSON parseable, it's a profile.
15772 var profileData = JSON.parse(response.responseText);
15773 } catch(error) {
15774 // if it is NOT JSON parseable, it's a 404 - user doesn't have a profile.
15775 var profileData = {};
15776 }
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);
15783 }
15784 });
15785 } else {
15786 this.displayProfile(username, this.profileCache[username]);
15787 }
15788 },
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>';
15798 }
15799 if (typeof profileObject.description !== 'undefined') {
15800 profileBody += '<h3>Description:</h3>';
15801 profileBody += '<div class="redditGiftsProfileField">'+profileObject.description+'</div>';
15802 }
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>';
15806 }
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>';
15810 }
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>';
15814 }
15815 if (typeof profileObject.trophies !== 'undefined') {
15816 profileBody += '<h3>RedditGifts Trophies:</h3>';
15817 var count=1;
15818 var len=profileObject.trophies.length;
15819 for (var i in profileObject.trophies) {
15820 var rowNum = parseInt(count/2);
15821 if (count===1) {
15822 profileBody += '<table class="trophy-table"><tbody>';
15823 }
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>';
15828 }
15829 if ((count===len) && ((count%2) === 1)) {
15830 profileBody += '<td class="trophy-info" colspan="2">';
15831 } else {
15832 profileBody += '<td class="trophy-info">';
15833 }
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>';
15838 }
15839 count++;
15840 }
15841 if (count) {
15842 profileBody += '</tbody></table>';
15843 }
15844 }
15845 if (profileBody === '') {
15846 profileBody = 'User has not filled out a profile on <a target="_blank" href="http://redditgifts.com">RedditGifts</a>.';
15847 }
15848 profileHTML += profileBody + '</div></div>';
15849 $(newSpacer).html(profileHTML);
15850 addClass(newSpacer,'spacer');
15851 insertAfter(firstSpacer,newSpacer);
15852 }
15853 }
15854 };
15855 */
15856
15857 modules['subredditManager'] = {
15858 moduleID: 'subredditManager',
15859 moduleName: 'Subreddit Manager',
15860 category: 'UI',
15861 options: {
15862 subredditShortcut: {
15863 type: 'boolean',
15864 value: true,
15865 description: 'Add +shortcut button in subreddit sidebar for easy addition of shortcuts.'
15866 },
15867 linkDashboard: {
15868 type: 'boolean',
15869 value: true,
15870 description: 'Show "DASHBOARD" link in subreddit manager'
15871 },
15872 linkAll: {
15873 type: 'boolean',
15874 value: true,
15875 description: 'Show "ALL" link in subreddit manager'
15876 },
15877 linkFront: {
15878 type: 'boolean',
15879 value: true,
15880 description: 'show "FRONT" link in subreddit manager'
15881 },
15882 linkRandom: {
15883 type: 'boolean',
15884 value: true,
15885 description: 'Show "RANDOM" link in subreddit manager'
15886 },
15887 linkMyRandom: {
15888 type: 'boolean',
15889 value: true,
15890 description: 'Show "MYRANDOM" link in subreddit manager (reddit gold only)'
15891 },
15892 linkRandNSFW: {
15893 type: 'boolean',
15894 value: false,
15895 description: 'Show "RANDNSFW" link in subreddit manager'
15896 },
15897 linkFriends: {
15898 type: 'boolean',
15899 value: true,
15900 description: 'Show "FRIENDS" link in subreddit manager'
15901 },
15902 linkMod: {
15903 type: 'boolean',
15904 value: true,
15905 description: 'Show "MOD" link in subreddit manager'
15906 },
15907 linkModqueue: {
15908 type: 'boolean',
15909 value: true,
15910 description: 'Show "MODQUEUE" link in subreddit manager'
15911 }
15912 /* sortingField: {
15913 type: 'enum',
15914 values: [
15915 { name: 'Subreddit Name', value: 'displayName' },
15916 { name: 'Added date', value: 'addedDate' }
15917 ],
15918 value : 'displayName',
15919 description: 'Field to sort subreddit shortcuts by'
15920 },
15921 sortingDirection: {
15922 type: 'enum',
15923 values: [
15924 { name: 'Ascending', value: 'asc' },
15925 { name: 'Descending', value: 'desc' }
15926 ],
15927 value : 'asc',
15928 description: 'Field to sort subreddit shortcuts by'
15929 }
15930 */ },
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);
15934 },
15935 include: [
15936 /^https?:\/\/([a-z]+)\.reddit\.com\/.*/i
15937 ],
15938 isMatchURL: function() {
15939 return RESUtils.isMatchURL(this.moduleID);
15940 },
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; }');
15967
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; }');
15973
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;}');
15998
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; }');
16002 }
16003 }
16004 },
16005 go: function() {
16006 if ((this.isEnabled()) && (this.isMatchURL())) {
16007
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;
16014 }
16015 }
16016 }
16017
16018 this.manageSubreddits();
16019 if (RESUtils.currentSubreddit() !== null) {
16020 this.setLastViewtime();
16021 }
16022 }
16023 },
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();
16040 } else {
16041 var isMulti = true;
16042 var thisSubredditFragment = $(subButton).next().text();
16043 }
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...)
16048 if (isMulti) {
16049 var theWrap = $(subButton).parent();
16050 $(theWrap).appendTo($(theWrap).parent());
16051 }
16052 }
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);
16056 },false);
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);
16061 var idx = -1;
16062 for (var i=0, sublen=modules['subredditManager'].mySubredditShortcuts.length; i<sublen; i++) {
16063 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === thisSubredditFragment.toLowerCase()) {
16064 idx=i;
16065 break;
16066 }
16067 }
16068 if (idx !== -1) {
16069 theSC.textContent = '-shortcut';
16070 theSC.setAttribute('title','Remove this subreddit from your shortcut bar');
16071 theSC.classList.add('remove');
16072 } else {
16073 theSC.textContent = '+shortcut';
16074 theSC.setAttribute('title','Add this subreddit to your shortcut bar');
16075 }
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');
16084 }
16085 }
16086 }
16087
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();
16091 }
16092 },
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')) {
16097 return;
16098 }
16099
16100 // Otherwise, indicate that this link now has a shortcut button
16101 $(this).data('hasShortcutButton', true);
16102
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;
16109
16110 for (var j = 0, shortcutsLength = modules['subredditManager'].mySubredditShortcuts.length; j < shortcutsLength; j++) {
16111 if (modules['subredditManager'].mySubredditShortcuts[j].subreddit === subreddit) {
16112 isShortcut = true;
16113 break;
16114 }
16115 }
16116
16117 if (isShortcut) {
16118 $theSC
16119 .attr('title', 'Remove this subreddit from your shortcut bar')
16120 .text('-shortcut')
16121 .addClass('remove');
16122 } else {
16123 $theSC
16124 .attr('title', 'Add this subreddit to your shortcut bar')
16125 .text('+shortcut')
16126 .removeClass('remove');
16127 }
16128
16129 $theSC
16130 .on('click', modules['subredditManager'].toggleSubredditShortcut)
16131 .appendTo($(this).find('.midcol'));
16132 });
16133 },
16134 redrawShortcuts: function() {
16135 this.shortCutsContainer.textContent = '';
16136 // Try Refresh subreddit shortcuts
16137 if (this.mySubredditShortcuts.length === 0) {
16138 this.getLatestShortcuts();
16139 }
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()
16148 }
16149 }
16150
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');
16156
16157 if ((RESUtils.currentSubreddit() !== null) && (RESUtils.currentSubreddit().toLowerCase() === this.mySubredditShortcuts[i].subreddit.toLowerCase())) {
16158 thisShortCut.classList.add('RESShortcutsCurrentSub');
16159 }
16160
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
16166 return true;
16167 } else {
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);
16173 }
16174 }
16175 }, false);
16176
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);
16182 }, false);
16183
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);
16189 }
16190 }, false);
16191
16192 thisShortCut.addEventListener('mouseout', function(e) {
16193 modules['subredditManager'].hideSubredditGroupDropdownTimer = setTimeout(function() {
16194 modules['subredditManager'].hideSubredditGroupDropdown();
16195 }, 500);
16196 }, false);
16197
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);
16205
16206 if (i < len - 1) {
16207 var sep = document.createElement('span');
16208 sep.setAttribute('class', 'separator');
16209 sep.textContent = '-';
16210 this.shortCutsContainer.appendChild(sep);
16211 }
16212 }
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';
16216 } else {
16217 this.shortCutsContainer.style.textTransform = '';
16218 }
16219 } else {
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 = [];
16223 }
16224 },
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);
16231
16232 this.subredditGroupDropdown.addEventListener('mouseout', function(e) {
16233 modules['subredditManager'].hideSubredditGroupDropdownTimer = setTimeout(function() {
16234 modules['subredditManager'].hideSubredditGroupDropdown();
16235 }, 500);
16236 }, false);
16237
16238 this.subredditGroupDropdown.addEventListener('mouseover', function(e) {
16239 clearTimeout(modules['subredditManager'].hideSubredditGroupDropdownTimer);
16240 }, false);
16241 }
16242 this.groupDropdownVisible = true;
16243
16244 if (subreddits) {
16245 $(this.subredditGroupDropdownUL).html('');
16246
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);
16250 }
16251
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';
16259
16260 modules['styleTweaks'].setSRStyleToggleVisibility(false, "subredditGroupDropdown");
16261 }
16262 },
16263 hideSubredditGroupDropdown: function() {
16264 delete modules['subredditManager'].hideSubredditGroupDropdownTimer;
16265 if (this.subredditGroupDropdown) {
16266 this.subredditGroupDropdown.style.display = 'none';
16267 modules['styleTweaks'].setSRStyleToggleVisibility(true, "subredditGroupDropdown")
16268 }
16269 },
16270 editSubredditShortcut: function(ele) {
16271 var subreddit = ele.getAttribute('href').slice(3);
16272
16273 var idx;
16274 for (var i=0, len=modules['subredditManager'].mySubredditShortcuts.length; i<len; i++) {
16275 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit == subreddit) {
16276 idx = i;
16277 break;
16278 }
16279 }
16280
16281 if (typeof this.editShortcutDialog === 'undefined') {
16282 this.editShortcutDialog = createElementWithID('div','editShortcutDialog');
16283 document.body.appendChild(this.editShortcutDialog);
16284 }
16285
16286 var thisForm = '<form name="editSubredditShortcut"> \
16287 <h3>Edit Shortcut</h3> \
16288 <div id="editShortcutClose" class="RESCloseButton">&times;</div> \
16289 <label for="subreddit">Subreddit:</label> \
16290 <input type="text" name="subreddit" value="' + subreddit + '" id="shortcut-subreddit"> \
16291 <br> \
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"> \
16296 </form>';
16297 $(this.editShortcutDialog).html(thisForm);
16298
16299 this.subredditInput = this.editShortcutDialog.querySelector('input[name=subreddit]');
16300 this.displayNameInput = this.editShortcutDialog.querySelector('input[name=displayName]');
16301
16302 this.subredditForm = this.editShortcutDialog.querySelector('FORM');
16303 this.subredditForm.addEventListener('submit', function(e) {
16304 e.preventDefault();
16305 }, false);
16306
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;
16312
16313 if ((subreddit === '') || (displayName === '')) {
16314 // modules['subredditManager'].mySubredditShortcuts.splice(idx,1);
16315 subreddit = modules['subredditManager'].mySubredditShortcuts[idx].subreddit;
16316 modules['subredditManager'].removeSubredditShortcut(subreddit);
16317 } else {
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());
16324 } else {
16325 var temp = { add: {}, del: {} };
16326 }
16327 modules['subredditManager'].RESPro = temp;
16328 }
16329 if (typeof modules['subredditManager'].RESPro.add === 'undefined') {
16330 modules['subredditManager'].RESPro.add = {}
16331 }
16332 if (typeof modules['subredditManager'].RESPro.del === 'undefined') {
16333 modules['subredditManager'].RESPro.del = {}
16334 }
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));
16344 }
16345 modules['subredditManager'].mySubredditShortcuts[idx] = {
16346 subreddit: subreddit,
16347 displayName: displayName,
16348 addedDate: new Date()
16349 }
16350
16351 modules['subredditManager'].saveLatestShortcuts();
16352
16353 if (RESUtils.proEnabled()) {
16354 modules['RESPro'].saveModuleData('subredditManager');
16355 }
16356 }
16357
16358 modules['subredditManager'].editShortcutDialog.style.display = 'none';
16359 modules['subredditManager'].redrawShortcuts();
16360 modules['subredditManager'].populateSubredditDropdown();
16361 }, false);
16362
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);
16370 }
16371 }, false);
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);
16378 }
16379 }, false);
16380
16381 var cancelButton = this.editShortcutDialog.querySelector('#editShortcutClose');
16382 cancelButton.addEventListener('click', function(e) {
16383 modules['subredditManager'].editShortcutDialog.style.display = 'none';
16384 }, false);
16385
16386 this.editShortcutDialog.style.display = 'block';
16387 var thisLeft = Math.min(RESUtils.mouseX, window.innerWidth - 300);
16388 this.editShortcutDialog.style.left = thisLeft + 'px';
16389
16390 setTimeout(function() {
16391 modules['subredditManager'].subredditInput.focus()
16392 }, 200);
16393 },
16394 followSubredditShortcut: function() {
16395 if (BrowserDetect.isFirefox()) {
16396 // stupid firefox... sigh...
16397 location.href = location.protocol + '//' + location.hostname + modules['subredditManager'].clickedShortcut;
16398 } else {
16399 location.href = modules['subredditManager'].clickedShortcut;
16400 }
16401 },
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;
16408
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');
16412 },
16413 subredditDragEnter: function(e) {
16414 this.classList.add('srOver');
16415 return false;
16416 },
16417 subredditDragOver: function(e) {
16418 if (e.preventDefault) {
16419 e.preventDefault(); // Necessary. Allows us to drop.
16420 }
16421
16422 // See the section on the DataTransfer object.
16423 e.dataTransfer.dropEffect = 'move';
16424 return false;
16425 },
16426 subredditDragLeave: function(e) {
16427 this.classList.remove('srOver');
16428 return false;
16429 },
16430 subredditDrop: function(e) {
16431 // this/e.target is current target element.
16432 if (e.stopPropagation) {
16433 e.stopPropagation(); // Stops some browsers from redirecting.
16434 }
16435
16436 // Stops other browsers from redirecting.
16437 e.preventDefault();
16438
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;
16452
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];
16456 rearrangedI++;
16457 } else if (i === destOrderIndex) {
16458 if (destOrderIndex > srcOrderIndex) {
16459 // if dragging right, order dest first, src next.
16460 rearranged[rearrangedI] = destSubreddit;
16461 rearrangedI++;
16462 rearranged[rearrangedI] = srcSubreddit;
16463 rearrangedI++;
16464 } else {
16465 // if dragging left, order src first, dest next.
16466 rearranged[rearrangedI] = srcSubreddit;
16467 rearrangedI++;
16468 rearranged[rearrangedI] = destSubreddit;
16469 rearrangedI++;
16470 }
16471 }
16472 }
16473
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');
16480 } else {
16481 var theData = modules['subredditManager'].srDataTransfer.split(',');
16482 var srcOrderIndex = parseInt(theData[0], 10);
16483 var srcSubreddit = theData[1];
16484 modules['subredditManager'].removeSubredditShortcut(srcSubreddit);
16485 }
16486 }
16487 return false;
16488 },
16489 subredditDragEnd: function(e) {
16490 modules['subredditManager'].shortCutsTrash.style.display = 'none';
16491 this.style.opacity = '1';
16492 return false;
16493 },
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('');
16501
16502 this.srLeftContainer = createElementWithID('div','srLeftContainer');
16503 this.srLeftContainer.setAttribute('class','sr-bar');
16504
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);
16510
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);
16516
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);
16522
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';
16531
16532 var shortCutsHTML = '';
16533
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>';
16540
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>';
16543
16544 var modmail = document.getElementById('modmail');
16545 if (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>';
16548 }
16549 }
16550 $(this.staticShortCutsContainer).append(shortCutsHTML);
16551
16552 this.srLeftContainer.appendChild(this.staticShortCutsContainer);
16553 this.srLeftContainer.appendChild(sep);
16554 this.headerContents.appendChild(this.srLeftContainer);
16555
16556 this.shortCutsViewport = document.createElement('div');
16557 this.shortCutsViewport.setAttribute('id','RESShortcutsViewport');
16558 this.headerContents.appendChild(this.shortCutsViewport);
16559
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);
16564
16565 this.shortCutsEditContainer = document.createElement('div');
16566 this.shortCutsEditContainer.setAttribute('id','RESShortcutsEditContainer');
16567 this.headerContents.appendChild(this.shortCutsEditContainer);
16568
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 = '&uarr;&darr;';
16574 this.sortShortcutsButton.addEventListener('click', modules['subredditManager'].showSortMenu, false);
16575 this.shortCutsEditContainer.appendChild(this.sortShortcutsButton);
16576
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);
16585
16586 if (isNaN(marginLeft)) marginLeft = 0;
16587
16588 var shiftWidth = $('#RESShortcutsViewport').width() - 80;
16589 if (modules['subredditManager'].containerWidth > (shiftWidth)) {
16590 marginLeft -= shiftWidth;
16591 modules['subredditManager'].shortCutsContainer.firstChild.style.marginLeft = marginLeft + 'px';
16592 }
16593 }, false);
16594 this.shortCutsEditContainer.appendChild(this.shortCutsRight);
16595
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';
16604 var thisForm = ' \
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> \
16611 </form> \
16612 ';
16613 $(this.shortCutsAddFormContainer).html(thisForm);
16614 this.shortCutsAddFormField = this.shortCutsAddFormContainer.querySelector('#newShortcut');
16615 this.shortCutsAddFormFieldDisplayName = this.shortCutsAddFormContainer.querySelector('#displayName');
16616
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();
16621 }
16622 }, false);
16623
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();
16628 }
16629 }, false);
16630
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;
16638
16639 var r_match_regex = /^(\/r\/|r\/)(.*)/i;
16640 if(r_match_regex.test(subreddit)) {
16641 subreddit = subreddit.match(r_match_regex)[2];
16642 }
16643
16644 modules['subredditManager'].shortCutsAddFormField.value = '';
16645 modules['subredditManager'].shortCutsAddFormFieldDisplayName.value = '';
16646 modules['subredditManager'].shortCutsAddFormContainer.style.display = 'none';
16647
16648 if (subreddit) {
16649 modules['subredditManager'].addSubredditShortcut(subreddit, displayname);
16650 }
16651 }, false);
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();
16656 } else {
16657 modules['subredditManager'].shortCutsAddFormContainer.style.display = 'none';
16658 modules['subredditManager'].shortCutsAddFormField.blur();
16659 }
16660 }, false);
16661 this.shortCutsEditContainer.appendChild(this.shortCutsAdd);
16662 document.body.appendChild(this.shortCutsAddFormContainer);
16663
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);
16673
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);
16681
16682 if (isNaN(marginLeft)) marginLeft = 0;
16683
16684 var shiftWidth = $('#RESShortcutsViewport').width() - 80;
16685 marginLeft += shiftWidth;
16686 if (marginLeft <= 0) {
16687 modules['subredditManager'].shortCutsContainer.firstChild.style.marginLeft = marginLeft + 'px';
16688 }
16689 }, false);
16690 this.shortCutsEditContainer.appendChild(this.shortCutsLeft);
16691
16692 this.redrawShortcuts();
16693 }
16694 },
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>&nbsp;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>' +
16703 '</div>');
16704
16705 $(modules['subredditManager'].sortMenu).find('a').click(modules['subredditManager'].sortShortcuts);
16706
16707 $(document.body).append(modules['subredditManager'].sortMenu);
16708 }
16709 var menu = modules['subredditManager'].sortMenu;
16710 if ($(menu).is(':visible')) {
16711 $(menu).hide();
16712 return;
16713 }
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();
16717
16718 $(menu).css({
16719 top: thisXY.top + thisHeight,
16720 left: thisXY.left
16721 }).show();
16722 },
16723 hideSortMenu: function() {
16724 var menu = modules['subredditManager'].sortMenu;
16725 $(menu).hide();
16726 },
16727 sortShortcuts: function(e) {
16728 modules['subredditManager'].hideSortMenu();
16729
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();
16737 }
16738
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();
16747 }
16748
16749 if (aField === bField) {
16750 return 0;
16751 } else if (aField > bField) {
16752 return (asc) ? 1 : -1;
16753 } else {
16754 return (asc) ? -1 : 1;
16755 }
16756 });
16757
16758 // Save shortcuts sort order
16759 modules['subredditManager'].saveLatestShortcuts();
16760
16761 // Refresh shortcuts
16762 modules['subredditManager'].redrawShortcuts();
16763 },
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);
16769 } else {
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');
16774 }
16775 modules['subredditManager'].srList.style.display = 'block';
16776 modules['subredditManager'].getSubreddits();
16777 } else {
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';
16780 }
16781 modules['subredditManager'].srList.addEventListener('click',modules['subredditManager'].stopDropDownPropagation, false);
16782 document.body.addEventListener('click',modules['subredditManager'].toggleSubredditDropdown, false);
16783 }
16784 },
16785 stopDropDownPropagation: function(e) {
16786 e.stopPropagation();
16787 },
16788 mySubreddits: [
16789 ],
16790 mySubredditShortcuts: [
16791 ],
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({
16796 method: "GET",
16797 url: jsonURL,
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';
16803 } else {
16804 var pages = modules['subredditManager'].subredditPagesLoaded.innerHTML.match(/:\ ([\d]+)/);
16805 modules['subredditManager'].subredditPagesLoaded.textContent = 'Pages loaded: ' + (parseInt(pages[1], 10)+1);
16806 }
16807
16808 var now = new Date();
16809 RESStorage.setItem('RESmodules.subredditManager.subreddits.lastCheck.'+RESUtils.loggedInUser(),now.getTime());
16810
16811 var subreddits = thisResponse.data.children;
16812 for (var i = 0, len = subreddits.length; i < len; i++) {
16813 var srObj = {
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
16820 }
16821 modules['subredditManager'].mySubreddits.push(srObj);
16822 }
16823
16824 if (thisResponse.data.after) {
16825 modules['subredditManager'].getSubredditJSON(thisResponse.data.after);
16826 } else {
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;
16832 return -1;
16833 });
16834
16835 RESStorage.setItem('RESmodules.subredditManager.subreddits.' + RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].mySubreddits));
16836 this.gettingSubreddits = false;
16837 modules['subredditManager'].populateSubredditDropdown();
16838 }
16839 } else {
16840 // User is probably not logged in.. no subreddits found.
16841 modules['subredditManager'].populateSubredditDropdown(null, true);
16842 }
16843 }
16844 });
16845
16846 },
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());
16852
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();
16858 }
16859 } else {
16860 modules['subredditManager'].mySubreddits = safeJSON.parse(check, 'RESmodules.subredditManager.subreddits.'+RESUtils.loggedInUser());
16861 this.populateSubredditDropdown();
16862 }
16863 },
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...
16869
16870 var theHead = document.createElement('thead');
16871 var theRow = document.createElement('tr');
16872
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');
16877 } else {
16878 modules['subredditManager'].populateSubredditDropdown('subreddit');
16879 }
16880 }, false);
16881 modules['subredditManager'].srHeader.textContent = 'subreddit';
16882 modules['subredditManager'].srHeader.setAttribute('width','200');
16883
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');
16888 } else {
16889 modules['subredditManager'].populateSubredditDropdown('lastVisited');
16890 }
16891 }, false);
16892 modules['subredditManager'].lvHeader.textContent = 'Last Visited';
16893 modules['subredditManager'].lvHeader.setAttribute('width','120');
16894
16895 var scHeader = document.createElement('td');
16896 $(scHeader).width(50);
16897 $(scHeader).html('<a style="float: right;" href="/subreddits/">View all &raquo;</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);
16903
16904 var theBody = document.createElement('tbody');
16905 if (!badJSON) {
16906 var subredditCount = modules['subredditManager'].mySubreddits.length;
16907
16908 if (typeof this.subredditsLastViewed === 'undefined') {
16909 var check = RESStorage.getItem('RESmodules.subredditManager.subredditsLastViewed.'+RESUtils.loggedInUser());
16910 if (check) {
16911 this.subredditsLastViewed = safeJSON.parse(check, 'RESmodules.subredditManager.subredditsLastViewed.'+RESUtils.loggedInUser());
16912 } else {
16913 this.subredditsLastViewed = {};
16914 }
16915 }
16916
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';
16922
16923 sortableSubreddits.sort(function(a, b) {
16924 var adisp = a.display_name.toLowerCase();
16925 var bdisp = b.display_name.toLowerCase();
16926
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);
16929
16930 if (alv < blv) return 1;
16931 if (alv == blv) {
16932 if (adisp > bdisp) return 1;
16933 return -1;
16934 }
16935 return -1;
16936 });
16937 } else if (sortBy === 'lastVisitedAsc') {
16938 $(modules['subredditManager'].lvHeader).html('Last Visited <div class="sortDesc"></div>');
16939 modules['subredditManager'].srHeader.textContent = 'subreddit';
16940
16941 sortableSubreddits.sort(function(a, b) {
16942 var adisp = a.display_name.toLowerCase();
16943 var bdisp = b.display_name.toLowerCase();
16944
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);
16947
16948 if (alv > blv) return 1;
16949 if (alv == blv) {
16950 if (adisp > bdisp) return 1;
16951 return -1;
16952 }
16953 return -1;
16954 });
16955 } else if (sortBy === 'subredditDesc') {
16956 modules['subredditManager'].lvHeader.textContent = 'Last Visited';
16957 $(modules['subredditManager'].srHeader).html('subreddit <div class="sortDesc"></div>');
16958
16959 sortableSubreddits.sort(function(a,b) {
16960 var adisp = a.display_name.toLowerCase();
16961 var bdisp = b.display_name.toLowerCase();
16962
16963 if (adisp < bdisp) return 1;
16964 if (adisp == bdisp) return 0;
16965 return -1;
16966 });
16967 } else {
16968 modules['subredditManager'].lvHeader.textContent = 'Last Visited';
16969 $(modules['subredditManager'].srHeader).html('subreddit <div class="sortAsc"></div>');
16970
16971 sortableSubreddits.sort(function(a,b) {
16972 var adisp = a.display_name.toLowerCase();
16973 var bdisp = b.display_name.toLowerCase();
16974
16975 if (adisp > bdisp) return 1;
16976 if (adisp == bdisp) return 0;
16977 return -1;
16978 });
16979 }
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);
16987 }
16988
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);
16993
16994 var theLV = document.createElement('td');
16995 theLV.textContent = dateString;
16996 theLV.setAttribute('class','RESvisited');
16997 theRow.appendChild(theLV);
16998
16999 var theSC = document.createElement('td');
17000 theSC.setAttribute('class','RESshortcut');
17001 theSC.setAttribute('data-subreddit',modules['subredditManager'].mySubreddits[i].display_name);
17002
17003 var idx = -1;
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) {
17006 idx = j;
17007 break;
17008 }
17009 }
17010
17011 if (idx !== -1) {
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...
17015 }
17016
17017 var subreddit = $(e.target).data('subreddit');
17018 modules['subredditManager'].removeSubredditShortcut(subreddit);
17019 }, false);
17020
17021 theSC.textContent = '-shortcut';
17022 } else {
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...
17026 }
17027
17028 var subreddit = $(e.target).data('subreddit');
17029 modules['subredditManager'].addSubredditShortcut(subreddit);
17030 }, false);
17031
17032 theSC.textContent = '+shortcut';
17033 }
17034
17035 theRow.appendChild(theSC);
17036 theBody.appendChild(theRow);
17037 }
17038 } else {
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');
17042
17043 var theRow = document.createElement('tr');
17044 theRow.appendChild(theTD);
17045 theBody.appendChild(theRow);
17046 }
17047
17048 modules['subredditManager'].srList.appendChild(theBody);
17049 },
17050 toggleSubredditShortcut: function(e) {
17051 e.stopPropagation(); // Stops from triggering the click on the bigger box, which toggles this window closed...
17052
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()) {
17056 isShortcut = true;
17057 break;
17058 }
17059 }
17060
17061 if (isShortcut) {
17062 modules['subredditManager'].removeSubredditShortcut($(this).data('subreddit'));
17063 $(this)
17064 .attr('title', 'Add this subreddit to your shortcut bar')
17065 .text('+shortcut')
17066 .removeClass('remove');
17067 } else {
17068 modules['subredditManager'].addSubredditShortcut($(this).data('subreddit'));
17069 $(this)
17070 .attr('title', 'Remove this subreddit from your shortcut bar')
17071 .text('-shortcut')
17072 .addClass('remove');
17073 }
17074
17075 modules['subredditManager'].redrawShortcuts();
17076 },
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());
17080 if (!shortCuts) {
17081 shortCuts = '[]';
17082 }
17083
17084 this.mySubredditShortcuts = safeJSON.parse(shortCuts, 'RESmodules.subredditManager.subredditShortcuts.' + RESUtils.loggedInUser());
17085 this.parseDates();
17086 },
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)
17092 : new Date(0);
17093 }
17094 },
17095 saveLatestShortcuts: function() {
17096 // Retreive the latest data to ensure we're not losing info
17097 if (!modules['subredditManager'].mySubredditShortcuts) {
17098 modules['subredditManager'].mySubredditShortcuts = [];
17099 }
17100
17101 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.' + RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].mySubredditShortcuts));
17102 },
17103 addSubredditShortcut: function(subreddit, displayname) {
17104 modules['subredditManager'].getLatestShortcuts();
17105
17106 var idx = -1;
17107 for (var i = 0, len=modules['subredditManager'].mySubredditShortcuts.length; i < len; i++) {
17108 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === subreddit.toLowerCase()) {
17109 idx = i;
17110 break;
17111 }
17112 }
17113
17114 if (idx !== -1) {
17115 alert('Whoops, you already have a shortcut for that subreddit');
17116 } else {
17117 displayname = displayname || subreddit;
17118 var subredditObj = {
17119 subreddit: subreddit,
17120 displayName: displayname.toLowerCase(),
17121 addedDate: new Date()
17122 }
17123
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());
17129 } else {
17130 var temp = { add: {}, del: {} };
17131 }
17132
17133 modules['subredditManager'].RESPro = temp;
17134 }
17135
17136 if (typeof modules['subredditManager'].RESPro.add === 'undefined') {
17137 modules['subredditManager'].RESPro.add = {}
17138 }
17139
17140 if (typeof modules['subredditManager'].RESPro.del === 'undefined') {
17141 modules['subredditManager'].RESPro.del = {}
17142 }
17143
17144 // add this subreddit next time we sync...
17145 modules['subredditManager'].RESPro.add[subreddit] = true;
17146
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];
17149
17150 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.RESPro.'+RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].RESPro));
17151 }
17152
17153 modules['subredditManager'].saveLatestShortcuts();
17154 modules['subredditManager'].redrawShortcuts();
17155 modules['subredditManager'].populateSubredditDropdown();
17156
17157 if (RESUtils.proEnabled()) {
17158 modules['RESPro'].saveModuleData('subredditManager');
17159 }
17160
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.'
17164 });
17165 }
17166 },
17167 removeSubredditShortcut: function(subreddit) {
17168 this.getLatestShortcuts();
17169
17170 var idx = -1;
17171 for (var i = 0, len = modules['subredditManager'].mySubredditShortcuts.length; i < len; i++) {
17172 if (modules['subredditManager'].mySubredditShortcuts[i].subreddit.toLowerCase() === subreddit.toLowerCase()) {
17173 idx = i;
17174 break;
17175 }
17176 }
17177
17178 if (idx !== -1) {
17179 modules['subredditManager'].mySubredditShortcuts.splice(idx, 1);
17180
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());
17185 } else {
17186 var temp = { add: {}, del: {} };
17187 }
17188
17189 modules['subredditManager'].RESPro = temp;
17190 }
17191 if (typeof modules['subredditManager'].RESPro.add === 'undefined') {
17192 modules['subredditManager'].RESPro.add = {}
17193 }
17194 if (typeof modules['subredditManager'].RESPro.del === 'undefined') {
17195 modules['subredditManager'].RESPro.del = {}
17196 }
17197
17198 // delete this subreddit next time we sync...
17199 modules['subredditManager'].RESPro.del[subreddit] = true;
17200
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];
17203
17204 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.RESPro.' + RESUtils.loggedInUser(), JSON.stringify(modules['subredditManager'].RESPro));
17205 }
17206
17207 modules['subredditManager'].saveLatestShortcuts();
17208 modules['subredditManager'].redrawShortcuts();
17209 modules['subredditManager'].populateSubredditDropdown();
17210
17211 if (RESUtils.proEnabled()) {
17212 modules['RESPro'].saveModuleData('subredditManager');
17213 }
17214 }
17215 },
17216 setLastViewtime: function() {
17217 var check = RESStorage.getItem('RESmodules.subredditManager.subredditsLastViewed.' + RESUtils.loggedInUser());
17218
17219 if (!check) {
17220 this.subredditsLastViewed = {};
17221 } else {
17222 this.subredditsLastViewed = safeJSON.parse(check, 'RESmodules.subredditManager.subredditsLastViewed.' + RESUtils.loggedInUser());
17223 }
17224
17225 var now = new Date();
17226 var thisReddit = RESUtils.currentSubreddit().toLowerCase();
17227 this.subredditsLastViewed[thisReddit] = {
17228 last_visited: now.getTime()
17229 }
17230
17231 RESStorage.setItem('RESmodules.subredditManager.subredditsLastViewed.' + RESUtils.loggedInUser(), JSON.stringify(this.subredditsLastViewed));
17232 },
17233 subscribeToSubreddit: function(subredditName, subscribe) {
17234 // subredditName should look like t5_123asd
17235 subscribe = subscribe !== false; // default to true
17236 var userHash = RESUtils.loggedInUserHash();
17237
17238 var formData = new FormData();
17239 formData.append('sr', subredditName);
17240 formData.append('action', subscribe ? 'sub' : 'unsub');
17241 formData.append('uh', userHash);
17242
17243 GM_xmlhttpRequest({
17244 method: "POST",
17245 url: location.protocol + "//"+location.hostname+"/api/subscribe?app=res",
17246 data: formData
17247 });
17248
17249 }
17250
17251 }; // note: you NEED this semicolon at the end!
17252
17253 // RES Pro needs some work still... not ready yet.
17254 /*
17255 modules['RESPro'] = {
17256 moduleID: 'RESPro',
17257 moduleName: 'RES Pro',
17258 category: 'Pro Features',
17259 options: {
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)
17263 // for example:
17264 username: {
17265 type: 'text',
17266 value: '',
17267 description: 'Your RES Pro username'
17268 },
17269 password: {
17270 type: 'password',
17271 value: '',
17272 description: 'Your RES Pro password'
17273 },
17274 syncFrequency: {
17275 type: 'enum',
17276 values: [
17277 { name: 'Hourly', value: '3600000' },
17278 { name: 'Daily', value: '86400000' },
17279 { name: 'Manual Only', value: '-1' }
17280 ],
17281 value: '86400000',
17282 description: 'How often should RES automatically sync settings?'
17283 }
17284 },
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);
17288 },
17289 include: [
17290 /^https?:\/\/([a-z]+)\.reddit\.com\/?/i,
17291 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+/i
17292 ],
17293 isMatchURL: function() {
17294 return RESUtils.isMatchURL(this.moduleID);
17295 },
17296 go: function() {
17297 if ((this.isEnabled()) && (this.isMatchURL())) {
17298 // do stuff now!
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);
17305 }
17306 }
17307
17308 }
17309 },
17310 autoSync: function() {
17311 modules['RESPro'].authenticate(modules['RESPro'].savePrefs);
17312
17313 // modules['RESPro'].authenticate(function() {
17314 // modules['RESPro'].saveModuleData('saveComments');
17315 // });
17316 },
17317 saveModuleData: function(module) {
17318 switch(module){
17319 case 'userTagger':
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({
17324 method: "POST",
17325 url: 'http://reddit.honestbleeps.com/RESsync.php',
17326 data: 'action=PUT&type=module_data&module='+module+'&data='+tags,
17327 headers: {
17328 "Content-Type": "application/x-www-form-urlencoded"
17329 },
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!';
17335 } else {
17336 alert(response.responseText);
17337 }
17338 }
17339 });
17340 break;
17341 case 'saveComments':
17342 var savedComments = RESStorage.getItem('RESmodules.saveComments.savedComments');
17343 GM_xmlhttpRequest({
17344 method: "POST",
17345 url: 'http://reddit.honestbleeps.com/RESsync.php',
17346 data: 'action=PUT&type=module_data&module='+module+'&data='+savedComments,
17347 headers: {
17348 "Content-Type": "application/x-www-form-urlencoded"
17349 },
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'
17363 });
17364 } else {
17365 alert(response.responseText);
17366 }
17367 }
17368 });
17369 break;
17370 case 'subredditManager':
17371 var subredditManagerData = {};
17372 subredditManagerData.RESPro = {};
17373
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);
17384 }
17385 }
17386 }
17387 var stringData = JSON.stringify(subredditManagerData);
17388 stringData = encodeURIComponent(stringData);
17389 GM_xmlhttpRequest({
17390 method: "POST",
17391 url: 'http://reddit.honestbleeps.com/RESsync.php',
17392 data: 'action=PUT&type=module_data&module='+module+'&data='+stringData,
17393 headers: {
17394 "Content-Type": "application/x-www-form-urlencoded"
17395 },
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'
17404 });
17405 } else {
17406 alert(response.responseText);
17407 }
17408 }
17409 });
17410 break;
17411 default:
17412 console.log('invalid module specified: ' + module);
17413 break;
17414 }
17415 },
17416 getModuleData: function(module) {
17417 switch(module){
17418 case 'saveComments':
17419 if (RESConsole.proSaveCommentsGetButton) RESConsole.proSaveCommentsGetButton.textContent = 'Loading...';
17420 GM_xmlhttpRequest({
17421 method: "POST",
17422 url: 'http://reddit.honestbleeps.com/RESsync.php',
17423 data: 'action=GET&type=module_data&module='+module,
17424 headers: {
17425 "Content-Type": "application/x-www-form-urlencoded"
17426 },
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];
17436 }
17437 }
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!';
17441 } else {
17442 alert(response.responseText);
17443 }
17444 }
17445 });
17446 break;
17447 case 'subredditManager':
17448 if (RESConsole.proSubredditManagerGetButton) RESConsole.proSubredditManagerGetButton.textContent = 'Loading...';
17449 GM_xmlhttpRequest({
17450 method: "POST",
17451 url: 'http://reddit.honestbleeps.com/RESsync.php',
17452 data: 'action=GET&type=module_data&module='+module,
17453 headers: {
17454 "Content-Type": "application/x-www-form-urlencoded"
17455 },
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;
17470 exists = true;
17471 break;
17472 }
17473 }
17474 if (!exists) {
17475 oldSubredditData.push(newSubredditData[newidx]);
17476 }
17477 }
17478 RESStorage.setItem('RESmodules.subredditManager.subredditShortcuts.'+username,JSON.stringify(oldSubredditData));
17479 }
17480 } else {
17481 alert(response.responseText);
17482 }
17483 }
17484 });
17485 break;
17486 default:
17487 console.log('invalid module specified: ' + module);
17488 break;
17489 }
17490 },
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('.');
17499 if (keySplit) {
17500 var keyRoot = keySplit[0];
17501 switch (keyRoot) {
17502 case 'RES':
17503 var thisNode = keySplit[1];
17504 if (thisNode === 'modulePrefs') {
17505 RESOptions[thisNode] = safeJSON.parse(RESStorage.getItem(i), i);
17506 }
17507 break;
17508 case 'RESoptions':
17509 var thisModule = keySplit[1];
17510 if (thisModule !== 'accountSwitcher') {
17511 RESOptions[thisModule] = safeJSON.parse(RESStorage.getItem(i), i);
17512 }
17513 break;
17514 default:
17515 //console.log('Not currently handling keys with root: ' + keyRoot);
17516 break;
17517 }
17518 }
17519 }
17520 }
17521 // Post options blob.
17522 var RESOptionsString = JSON.stringify(RESOptions);
17523 GM_xmlhttpRequest({
17524 method: "POST",
17525 url: 'http://reddit.honestbleeps.com/RESsync.php',
17526 data: 'action=PUT&type=all_options&data='+RESOptionsString,
17527 headers: {
17528 "Content-Type": "application/x-www-form-urlencoded"
17529 },
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.'
17540 });
17541 } else {
17542 alert(response.responseText);
17543 }
17544 }
17545 });
17546 },
17547 getPrefs: function() {
17548 console.log('get prefs called');
17549 if (RESConsole.proGetButton) RESConsole.proGetButton.textContent = 'Loading...';
17550 GM_xmlhttpRequest({
17551 method: "POST",
17552 url: 'http://reddit.honestbleeps.com/RESsync.php',
17553 data: 'action=GET&type=all_options',
17554 headers: {
17555 "Content-Type": "application/x-www-form-urlencoded"
17556 },
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));
17568 } else {
17569 var thisOptions = prefsData[thisModule];
17570 RESStorage.setItem('RESoptions.'+thisModule,JSON.stringify(thisOptions));
17571 }
17572 }
17573 if (RESConsole.proGetButton) RESConsole.proGetButton.textContent = 'Preferences Loaded!';
17574 RESUtils.notification({
17575 header: 'RES Pro Notification',
17576 message: 'Module options loaded.'
17577 });
17578 // console.log(response.responseText);
17579 } else {
17580 alert(response.responseText);
17581 }
17582 }
17583 });
17584 },
17585 configure: function() {
17586 if (!RESConsole.isOpen) RESConsole.open();
17587 RESConsole.menuClick(document.getElementById('Menu-'+this.category));
17588 RESConsole.drawConfigOptions('RESPro');
17589 },
17590 authenticate: function(callback) {
17591 if (! this.isEnabled()) {
17592 return false;
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({
17598 method: "POST",
17599 url: 'http://reddit.honestbleeps.com/RESlogin.php',
17600 data: 'uname='+modules['RESPro'].options.username.value+'&pwd='+modules['RESPro'].options.password.value,
17601 headers: {
17602 "Content-Type": "application/x-www-form-urlencoded"
17603 },
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');
17609 if (callback) {
17610 callback();
17611 }
17612 } else {
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.'
17619 });
17620 }
17621 }
17622 });
17623 }
17624 }
17625 }
17626 };
17627 */
17628 modules['RESTips'] = {
17629 moduleID: 'RESTips',
17630 moduleName: 'RES Tips and Tricks',
17631 category: 'UI',
17632 options: {
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)
17636 // for example:
17637 dailyTip: {
17638 type: 'boolean',
17639 value: true,
17640 description: 'Show a random tip once every 24 hours.'
17641 }
17642 },
17643 description: 'Adds tips/tricks help to RES console',
17644 isEnabled: function() {
17645 return RESConsole.getModulePrefs(this.moduleID);
17646 },
17647 include: [
17648 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
17649 ],
17650 isMatchURL: function() {
17651 return RESUtils.isMatchURL(this.moduleID);
17652 },
17653 beforeLoad: function() {
17654 if (this.isEnabled() && this.isMatchURL()) {
17655 RESUtils.addCSS('.res-help { cursor: help; }');
17656 RESUtils.addCSS('.res-help #resHelp { cursor: default; }');
17657 }
17658 },
17659 go: function() {
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();
17665 }, false);
17666
17667 $('#RESDropdownOptions').append(this.menuItem);
17668
17669 if (this.options.dailyTip.value) {
17670 this.dailyTip();
17671 }
17672
17673 /*
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.",
17678 id: "first",
17679 // next: "second",
17680 overlay: true,
17681 xButton: true,
17682 title: "Welcome to Guiders.js!"
17683 }).show();
17684 */
17685 /*
17686 setTimeout(function() {
17687 guiders.createGuider({
17688 attachTo: "#RESSettingsButton",
17689 buttons: [{name: "Close"},
17690 {name: "Next"}],
17691 description: "This is just some sorta test guider, here... woop woop.",
17692 id: "first",
17693 next: "second",
17694 // offset: { left: -200, top: 120 },
17695 position: 5,
17696 title: "Guiders are typically attached to an element on the page."
17697 }).show();
17698 guiders.createGuider({
17699 attachTo: "a.toggleImage:first",
17700 buttons: [{name: "Close"},
17701 {name: "Next"}],
17702 description: "An example of an image expando",
17703 id: "second",
17704 next: "third",
17705 // offset: { left: -200, top: 120 },
17706 position: 3,
17707 title: "Guiders are typically attached to an element on the page."
17708 });
17709 }, 2000);
17710 */
17711 }
17712 },
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) {
17721 this.showTip(0);
17722 } else {
17723 setTimeout(function() {
17724 modules['RESTips'].randomTip();
17725 }, 500);
17726 }
17727 }
17728 },
17729 randomTip: function() {
17730 this.currTip = Math.floor(Math.random()*this.tips.length);
17731 this.showTip(this.currTip);
17732 },
17733 disableDailyTipsCheckbox: function(e) {
17734 modules['RESTips'].options.dailyTip.value = e.target.checked;
17735 RESStorage.setItem('RESoptions.RESTips', JSON.stringify(modules['RESTips'].options));
17736 },
17737 nextTip: function() {
17738 if (typeof this.currTip === 'undefined') this.currTip = 0;
17739 modules['RESTips'].nextPrevTip(1);
17740 },
17741 prevTip: function() {
17742 if (typeof this.currTip === 'undefined') this.currTip = 0;
17743 modules['RESTips'].nextPrevTip(-1);
17744 },
17745 nextPrevTip: function(idx) {
17746 if (typeof this.currTip === 'undefined') this.currTip = 0;
17747 // if (idx<0) guiders.hideAll();
17748 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) {
17753 this.currTip = 0;
17754 }
17755 this.showTip(this.currTip);
17756 },
17757 generateContent: function(help, elem) {
17758 var description = []
17759
17760 if (help.message) description.push(help.message);
17761
17762 if (help.keyboard) {
17763
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>');
17769
17770 var keyboardTable = RESUtils.generateTable(help.keyboard, this.generateContentKeyboard, elem);
17771 if (keyboardTable) description.push(keyboardTable);
17772 }
17773
17774 if (help.option) {
17775 description.push('<h2 class="settingsPointer">');
17776 description.push('<span class="gearIcon"></span> RES Settings');
17777 description.push('</h2>');
17778
17779 var optionTable = RESUtils.generateTable(help.option, this.generateContentOption, elem);
17780 if (optionTable) description.push(optionTable);
17781 }
17782
17783 description = description.join("\n");
17784 return description;
17785 },
17786 generateContentKeyboard: function (keyboardNavOption, index, array, elem) {
17787 var keyCode = modules['keyboardNav'].getNiceKeyCode(keyboardNavOption);
17788 if (!keyCode) return;
17789
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>&nbsp;</td>'); // for styling
17796 description.push('<td>' + modules['keyboardNav'].options[keyboardNavOption].description + '</td>');
17797 description.push('</tr>');
17798
17799 return description;
17800 },
17801 generateContentOption: function (option, index, array, elem) {
17802 var module = modules[option.moduleID];
17803 if (!module) return;
17804
17805 var description = [];
17806
17807 description.push("<tr>");
17808 description.push("<td>" + module.category + '</td>');
17809
17810 description.push('<td>');
17811 description.push(modules['settingsNavigation'].makeUrlHashLink(option.moduleID, null, module.moduleName));
17812 description.push('</td>');
17813
17814 description.push('<td>');
17815 description.push(option.key
17816 ? modules['settingsNavigation'].makeUrlHashLink(option.moduleID, option.key)
17817 : '&nbsp;');
17818 description.push('</td>');
17819
17820 if (module.options[option.key]) {
17821 description.push('</tr><tr>');
17822 description.push('<td colspan="3">' + module.options[option.key].description + '</td>');
17823 }
17824 description.push("</tr>");
17825
17826 return description;
17827 },
17828 consoleTip: {
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",
17831 position: 5
17832 },
17833 tips: [
17834 {
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",
17837 position: 5
17838 },
17839 {
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",
17842 position: 3,
17843 option: { moduleID: 'userTagger' }
17844 },
17845 {
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>"
17847 },
17848 {
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' }
17851 },
17852 {
17853 message: "Keyboard Navigation is one of the most underutilized features in RES. You should try it!" ,
17854 option: { moduleID: 'keyboardNav' },
17855 keyboard: 'toggleHelp'
17856 },
17857 {
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' }]
17860 },
17861
17862 {
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' }
17865 },
17866 {
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'
17870 },
17871 {
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' }
17874 },
17875 {
17876 message: "Hover over the 'parent' link in comments pages to see the text of the parent being referred to." ,
17877 option: { moduleID: 'showParent' }
17878 },
17879 {
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' }
17882 },
17883 {
17884 message: "Not a fan of how comments pages look? You can change the appearance in the Style Tweaks module" ,
17885 option: { moduleID: 'styleTweaks' }
17886 },
17887 {
17888 message: "Don't like the style in a certain subreddit? RES gives you a checkbox to disable styles individually - check the right sidebar!"
17889 },
17890 {
17891 message: "Looking for posts by submitter, post with photos, or posts in IAmA form? Try out the comment navigator."
17892 },
17893 {
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' }
17896 },
17897 {
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' }
17900 },
17901 {
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' }
17904 },
17905 {
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' }
17908 }
17909 ],
17910 tour: [
17911 // array of guiders will go here... and we will add a "tour" button somewhere to start the tour...
17912 ],
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);
17919 }
17920 },
17921 createGuider: function(i, special) {
17922 if (special === 'console') {
17923 var thisID = special;
17924 var thisTip = this.consoleTip;
17925 } else {
17926 var thisID = "tip"+i;
17927 var thisTip = this.tips[i];
17928 }
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"' : '';
17934 var guiderObj = {
17935 attachTo: attachTo,
17936 buttons: [{
17937 name: "Prev",
17938 onclick: modules['RESTips'].prevTip
17939 },
17940 {
17941 name: "Next",
17942 onclick: modules['RESTips'].nextTip
17943 }
17944 ],
17945 description: description,
17946 buttonCustomHTML: "<label class='stopper'> <input type='checkbox' name='disableDailyTipsCheckbox' id='disableDailyTipsCheckbox' "+thisChecked+" />Show these tips once every 24 hours</label>",
17947 id: thisID,
17948 next: nextID,
17949 onShow: modules['RESTips'].onShow,
17950 onHide: modules['RESTips'].onHide,
17951 position: this.tips[i].position,
17952 xButton: true,
17953 title: "RES Tips and Tricks"
17954 };
17955 if (special === 'console') {
17956 delete guiderObj.buttonCustomHTML;
17957 delete guiderObj.next;
17958 delete guiderObj.buttons;
17959
17960 guiderObj.title = "RES is extremely configurable";
17961
17962 }
17963
17964 guiders.createGuider(guiderObj);
17965 },
17966 showTip: function(idx, special) {
17967 if (typeof this.tipsInitialized === 'undefined') {
17968 this.initTips();
17969 this.tipsInitialized = true;
17970 }
17971 if (!special) {
17972 guiders.show('tip'+idx);
17973 } else {
17974 guiders.show('console');
17975 }
17976 },
17977 onShow: function() {
17978 modules['styleTweaks'].setSRStyleToggleVisibility(false, 'tipstricks');
17979 },
17980 onHide: function() {
17981 modules['styleTweaks'].setSRStyleToggleVisibility(true, 'tipstricks');
17982 }
17983 };
17984
17985
17986 modules['settingsNavigation'] = {
17987 moduleID: 'settingsNavigation',
17988 moduleName: 'RES Settings Navigation',
17989 category: 'UI',
17990 description: 'Helping you get around the RES Settings Console with greater ease',
17991 hidden: true,
17992 options: {
17993 },
17994 isEnabled: function() {
17995 // return RESConsole.getModulePrefs(this.moduleID);
17996 return true;
17997 },
17998 include: [
17999 /^https?:\/\/([-\w\.]+\.)?reddit\.com\/[-\w\.\/]*/i
18000 ],
18001 isMatchURL: function() {
18002 return RESUtils.isMatchURL(this.moduleID);
18003 },
18004 beforeLoad: function() {
18005 RESUtils.addCSS('#RESSearchMenuItem { \
18006 display: block; \
18007 float: right; \
18008 margin: 7px; \
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 + '; \
18014 } ');
18015 RESUtils.addCSS('li:hover > #RESSearchMenuItem { \
18016 border-color: #369; \
18017 background-image: ' + this.searchButtonIconHover + '; \
18018 }');
18019 },
18020 go: function() {
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()
18026 }, false);
18027 RESConsole.settingsButton.appendChild(this.menuItem);
18028
18029 if (!(this.isEnabled() && this.isMatchURL())) return;
18030
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
18034
18035 this.consoleTip();
18036 },
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');
18042 if (lastToolTip) {
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');
18054 }
18055 }
18056 }
18057 },
18058 makeUrlHashLink: function (moduleID, optionKey, displayText, cssClass) {
18059 if (!displayText) {
18060 if (optionKey) {
18061 displayText = optionKey;
18062 } else if (modules[moduleID]) {
18063 displayText = modules[moduleID].moduleName;
18064 } else if (moduleID) {
18065 displayText = moduleID;
18066 } else {
18067 displayText = 'Settings';
18068 }
18069 }
18070
18071 var hash = modules['settingsNavigation'].makeUrlHash(moduleID, optionKey);
18072 var link = ['<a ', 'class="', cssClass || '', '" ', 'href="', hash, '"', '>', displayText, '</a>'].join('');
18073 return link;
18074 },
18075 makeUrlHash: function(moduleID, optionKey) {
18076 var hashComponents = ['#!settings']
18077
18078 if (moduleID) {
18079 hashComponents.push(moduleID);
18080 }
18081
18082 if (moduleID && optionKey) {
18083 hashComponents.push(optionKey);
18084 }
18085
18086 var hash = hashComponents.join('/');
18087 return hash;
18088 },
18089 setUrlHash: function(moduleID, optionKey) {
18090 var titleComponents = ['RES Settings'];
18091
18092 if (moduleID) {
18093 var module = modules[moduleID];
18094 var moduleName = module && module.moduleName || moduleID;
18095 titleComponents.push(moduleName);
18096
18097 if (optionKey) {
18098 titleComponents.push(optionKey);
18099 }
18100 }
18101
18102 var hash = this.makeUrlHash(moduleID, optionKey);
18103 var title = titleComponents.join(' - ');
18104
18105 if (window.location.hash != hash) {
18106 window.history.pushState(hash, title, hash);
18107 }
18108 },
18109 resetUrlHash: function() {
18110 window.location.hash = "";
18111 },
18112 onHashChange: function (event) {
18113 var hash = window.location.hash;
18114 if (hash.substring(0, 10) !== '#!settings') return;
18115
18116 var params = hash.match(/\/[\w\s]+/g);
18117 if (params && params[0]) {
18118 var moduleID = params[0].substring(1);
18119 }
18120 if (params && params[1]) {
18121 var optionKey = params[1].substring(1);
18122 }
18123
18124 modules['settingsNavigation'].loadSettingsPage(moduleID, optionKey);
18125 },
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();
18131 }
18132 return;
18133 }
18134
18135 var moduleID = state[1];
18136 var optionKey = state[2];
18137
18138 modules['settingsNavigation'].loadSettingsPage(moduleID, optionKey);
18139 },
18140 loadSettingsPage: function(moduleID, optionKey, optionValue) {
18141 if (moduleID && modules.hasOwnProperty(moduleID)) {
18142 var module = modules[moduleID];
18143 }
18144 if (module) {
18145 var category = module.category;
18146 }
18147
18148
18149 RESConsole.open(module && module.moduleID);
18150 if (module) {
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);
18160 }
18161 }
18162 } else {
18163 switch(moduleID) {
18164 case 'search':
18165 this.search(optionKey);
18166 break;
18167 default:
18168 break;
18169 }
18170 }
18171 },
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);
18177 },
18178 showSearch: function () {
18179 RESConsole.hidePrefsDropdown();
18180 modules['settingsNavigation'].drawSearchResults();
18181 $('#SearchRES-input').focus();
18182 },
18183 doneSearch: function (query, results) {
18184 modules['settingsNavigation'].drawSearchResults(query, results);
18185 },
18186 getSearchResults: function (query) {
18187 if (!(query && query.toString().length)) {
18188 modules['settingsNavigation'].doneSearch(query, []);
18189 }
18190
18191 var queryTerms = modules['settingsNavigation'].prepareSearchText(query, true).split(' ');
18192 var results = [];
18193
18194 // Search options
18195 for (var moduleKey in modules) {
18196 if (!modules.hasOwnProperty(moduleKey)) continue;
18197 var module = modules[moduleKey];
18198
18199
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);
18203 if (matches) {
18204 var result = modules['settingsNavigation'].makeModuleSearchResult(moduleKey);
18205 result.rank = matches;
18206 results.push(result);
18207 }
18208
18209
18210 var options = module.options;
18211
18212 for (var optionKey in options) {
18213 if (!options.hasOwnProperty(optionKey)) continue;
18214 var option = options[optionKey];
18215
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);
18219 if (matches) {
18220 var result = modules['settingsNavigation'].makeOptionSearchResult(moduleKey, optionKey);
18221 result.rank = matches;
18222 results.push(result);
18223 }
18224 }
18225 }
18226
18227 results.sort(function(a, b) {
18228 var comparison = b.rank - a.rank;
18229
18230 /*
18231 if (comparison === 0) {
18232 comparison =
18233 a.title < b.title ? -1
18234 : a.title > b.title ? 1
18235 : 0;
18236
18237 }
18238
18239 if (comparison === 0) {
18240 comparison =
18241 a.description < b.description ? -1
18242 : a.description > b.description ? 1
18243 : 0;
18244 }
18245 */
18246
18247 return comparison;
18248 });
18249
18250 modules['settingsNavigation'].doneSearch(query, results);
18251
18252 },
18253 searchMatches: function(needles, haystack) {
18254 if (!(haystack && haystack.length))
18255 return false;
18256
18257 var numMatches = 0;
18258 for (var i = 0; i < needles.length; i++) {
18259 if (haystack.indexOf(needles[i]) !== -1)
18260 numMatches++;
18261 }
18262
18263 return numMatches;
18264 },
18265 prepareSearchText: function (text, preserveSpaces) {
18266 if (typeof text === "undefined" || text === null) {
18267 return '';
18268 }
18269
18270 var replaceSpacesWith = !!preserveSpaces ? ' ' : ''
18271 return text.toString().toLowerCase()
18272 .replace(/[,\/]/g,replaceSpacesWith).replace(/\s+/g, replaceSpacesWith);
18273 },
18274 makeOptionSearchResult: function (moduleKey, optionKey) {
18275 var module = modules[moduleKey];
18276 var option = module.options[optionKey];
18277
18278 var result = {};
18279 result.type = 'option';
18280 result.breadcrumb = ['Settings',
18281 module.category,
18282 module.moduleName + ' (' + module.moduleID + ')'
18283 ].join(' > ');
18284 result.title = optionKey;
18285 result.description = option.description;
18286 result.moduleID = moduleKey;
18287 result.optionKey = optionKey;
18288
18289 return result;
18290 },
18291 makeModuleSearchResult: function (moduleKey) {
18292 var module = modules[moduleKey];
18293
18294 var result = {};
18295 result.type = 'module';
18296 result.breadcrumb = ['Settings',
18297 module.category,
18298 '(' + module.moduleID + ')'
18299 ].join(' > ');
18300 result.title = module.moduleName;
18301 result.description = module.description;
18302 result.moduleID = moduleKey;
18303
18304 return result;
18305 },
18306
18307 onSearchResultSelected: function(result) {
18308 if (!result) return;
18309
18310 switch (result.type) {
18311 case 'module':
18312 modules['settingsNavigation'].loadSettingsPage(result.moduleID);
18313 break;
18314 case 'option':
18315 modules['settingsNavigation'].loadSettingsPage(result.moduleID, result.optionKey);
18316 break;
18317 default:
18318 alert('Could not load search result');
18319 break;
18320 }
18321 },
18322 // ---------- View ------
18323 css: '\
18324 #SearchRES #SearchRES-results-container { \
18325 display: none; \
18326 } \
18327 #SearchRES #SearchRES-results-container + #SearchRES-boilerplate { margin-top: 1em; border-top: 1px black solid; padding-top: 1em; } \
18328 #SearchRES h4 { \
18329 margin-top: 1.5em; \
18330 } \
18331 #SearchRES-results { \
18332 } \
18333 #SearchRES-results li { \
18334 list-style-type: none; \
18335 border-bottom: 1px dashed #ccc; \
18336 cursor: pointer; \
18337 margin-left: 0px; \
18338 padding-left: 10px; \
18339 padding-top: 24px; \
18340 padding-bottom: 24px; \
18341 } \
18342 #SearchRES-results li:hover { \
18343 background-color: #FAFAFF; \
18344 } \
18345 .SearchRES-result-title { \
18346 margin-bottom: 12px; \
18347 font-weight: bold; \
18348 color: #666; \
18349 } \
18350 .SearchRES-breadcrumb { \
18351 font-weight: normal; \
18352 color: #888; \
18353 } \
18354 .SearchRES-result-copybutton {\
18355 float: right; \
18356 opacity: 0.4; \
18357 padding: 10px; \
18358 width: 26px; \
18359 height: 22px; \
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="); \
18362 display: none; \
18363 } \
18364 #SearchRES-results li:hover .SearchRES-result-copybutton { display: block; } \
18365 #SearchRES-input-submit { \
18366 margin-left: 8px; \
18367 } \
18368 #SearchRES-input { \
18369 width: 200px; \
18370 height: 22px; \
18371 font-size: 14px; \
18372 } \
18373 #SearchRES-input-container { \
18374 float: left; \
18375 margin-left: 3em; \
18376 margin-top: 7px; \
18377 } \
18378 ',
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> \
18385 </div> \
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>\
18388 <ul> \
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> \
18391 </ul> \
18392 ',
18393 searchPanel: null,
18394 renderSearchPanel: function() {
18395 var searchPanel = $('<div />').html(modules['settingsNavigation'].searchPanelHtml);
18396 searchPanel.delegate('#SearchRES-results-container .SearchRES-result-item', 'click', modules['settingsNavigation'].handleSearchResultClick);
18397
18398 modules['settingsNavigation'].searchPanel = searchPanel;
18399 return searchPanel;
18400 },
18401 searchForm: null,
18402 renderSearchForm: function() {
18403 var RESSearchContainer = createElementWithID('form', 'SearchRES-input-container');
18404
18405 var RESSearchBox = createElementWithID('input', 'SearchRES-input');
18406 RESSearchBox.setAttribute('type', 'text');
18407 RESSearchBox.setAttribute('placeholder', 'search RES settings');
18408
18409 var RESSearchButton = createElementWithID('input', 'SearchRES-input-submit');
18410 RESSearchButton.classList.add('blueButton');
18411 RESSearchButton.setAttribute('type', 'submit');
18412 RESSearchButton.setAttribute('value', 'search');
18413
18414 RESSearchContainer.appendChild(RESSearchBox);
18415 RESSearchContainer.appendChild(RESSearchButton);
18416
18417 RESSearchContainer.addEventListener('submit', function (e) {
18418 e.preventDefault();
18419 modules['settingsNavigation'].search(RESSearchBox.value);
18420
18421 return false;
18422 });
18423
18424 searchForm = RESSearchContainer;
18425 return RESSearchContainer;
18426 },
18427 drawSearchResultsPage: function() {
18428 if (!RESConsole.isOpen) {
18429 RESConsole.open();
18430 }
18431
18432 if (!$('#SearchRES').is(':visible')) {
18433 RESConsole.openCategoryPanel('About RES');
18434
18435 // Open "Search RES" page
18436 $('#Button-SearchRES', this.RESConsoleContent).trigger('click', { duration: 0 });
18437 }
18438 },
18439 drawSearchResults: function (query, results) {
18440 modules['settingsNavigation'].drawSearchResultsPage();
18441
18442 var resultsContainer = $('#SearchRES-results-container', modules['settingsNavigation'].searchPanel);
18443
18444 if (!(query && query.length)) {
18445 resultsContainer.hide();
18446 return;
18447 }
18448
18449 resultsContainer.show();
18450 resultsContainer.find('#SearchRES-query').text(query);
18451 $("#SearchRES-input", modules['settingsNavigation'].searchForm).val(query);
18452
18453 if (!(results && results.length)) {
18454 resultsContainer.find('#SearchRES-results-none').show();
18455 resultsContainer.find('#SearchRES-results').hide();
18456 } else {
18457 resultsContainer.find('#SearchRES-results-none').hide();
18458 var resultsList = $('#SearchRES-results', resultsContainer).show();
18459
18460 resultsList.empty();
18461 for (var i = 0; i < results.length; i++) {
18462 var result = results[i];
18463
18464 var element = modules['settingsNavigation'].drawSearchResultItem(result);
18465 resultsList.append(element);
18466 }
18467 }
18468 },
18469 drawSearchResultItem: function(result) {
18470 var element = $('<li>');
18471 element.addClass('SearchRES-result-item')
18472 .data('SearchRES-result', result);
18473
18474 $('<span>', { class: 'SearchRES-result-copybutton'})
18475 .appendTo(element)
18476 .attr('title', 'copy this for a comment')
18477
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'})
18485 .appendTo(element)
18486 .html(result.description);
18487
18488 return element;
18489 },
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);
18495 } else {
18496 modules['settingsNavigation'].onSearchResultSelected(result);
18497 }
18498 e.preventDefault();
18499 },
18500 onSearchResultCopy: function(result, element) {
18501 var markdown = modules['settingsNavigation'].makeOptionSearchResultLink(result);
18502 alert('<textarea rows="5" cols="50">' + markdown + '</textarea>');
18503 },
18504 makeOptionSearchResultLink: function (result) {
18505 var url = document.location.pathname +
18506 modules['settingsNavigation'].makeUrlHash(result.moduleID, result.optionKey);
18507
18508 var text = [
18509 result.breadcrumb,
18510 '[' + result.title + '](' + url + ')',
18511 ' \n',
18512 result.description,
18513 ' \n',
18514 ' \n'
18515 ].join(' ');
18516 return text;
18517 }
18518 };
18519
18520
18521
18522
18523 modules['dashboard'] = {
18524 moduleID: 'dashboard',
18525 moduleName: 'RES Dashboard',
18526 category: 'UI',
18527 options: {
18528 defaultPosts: {
18529 type: 'text',
18530 value: 3,
18531 description: 'Number of posts to show by default in each widget'
18532 },
18533 defaultSort: {
18534 type: 'enum',
18535 values: [
18536 { name: 'hot', value: 'hot' },
18537 { name: 'new', value: 'new' },
18538 { name: 'controversial', value: 'controversial' },
18539 { name: 'top', value: 'top' }
18540 ],
18541 value: 'hot',
18542 description: 'Default sort method for new widgets'
18543 },
18544 dashboardShortcut: {
18545 type: 'boolean',
18546 value: true,
18547 description: 'Show +dashboard shortcut in sidebar for easy addition of dashboard widgets.'
18548 },
18549 tagsPerPage: {
18550 type: 'text',
18551 value: 25,
18552 description: 'How many user tags to show per page. (enter zero to show all on one page)'
18553 }
18554 },
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);
18558 },
18559 include: [
18560 /^https?:\/\/([-\w\.]+\.)?reddit\.com\/[-\w\.\/]*/i
18561 ],
18562 isMatchURL: function() {
18563 return RESUtils.isMatchURL(this.moduleID);
18564 },
18565 go: function() {
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)) {
18576 this.widgets = [];
18577 }
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();
18582 }
18583 if (this.options.dashboardShortcut.value == true) this.addDashboardShortcuts();
18584 }
18585 }
18586 }
18587 },
18588 getLatestWidgets: function() {
18589 try {
18590 this.widgets = JSON.parse(RESStorage.getItem('RESmodules.dashboard.' + RESUtils.loggedInUser())) || [];
18591 } catch (e) {
18592 this.widgets = [];
18593 }
18594 },
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;}');
18644
18645 var dbLinks = $('span.redditname a');
18646 if ($(dbLinks).length > 1) {
18647 $(dbLinks[0]).addClass('active');
18648 }
18649
18650 // add each subreddit widget...
18651 // add the "add widget" form...
18652 this.attachContainer();
18653 this.attachAddComponent();
18654 this.attachEditComponent();
18655 this.initUpdateQueue();
18656 },
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>"
18666 });
18667 }, 300);
18668 },
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);
18674 }
18675 },
18676 processUpdateQueue: function() {
18677 var thisUpdate = modules['dashboard'].updateQueue.pop();
18678 thisUpdate();
18679 if (modules['dashboard'].updateQueue.length < 1) {
18680 clearInterval(modules['dashboard'].updateQueueTimer);
18681 delete modules['dashboard'].updateQueueTimer;
18682 }
18683 },
18684 saveOrder: function() {
18685 var data = $("#siteTable li.RESDashboardComponent").map(function() { return $(this).attr("id"); }).get();
18686 data.reverse();
18687 var newOrder = [];
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];
18691 }
18692 modules['dashboard'].widgets = newOrder;
18693 delete newOrder;
18694 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
18695 },
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();
18705 } else {
18706 $('#userTaggerContents').hide();
18707 }
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();
18715 });
18716 },
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> \
18724 </div> \
18725 ');
18726 var thisEle = $(this.dashboardEditComponent).find('#editReddit');
18727
18728 $(this.dashboardEditComponent).find('#editRedditForm').submit(
18729 function(e) {
18730 e.preventDefault();
18731 var thisBasePath = $('#editReddit').val();
18732 if (thisBasePath !== '') {
18733 if (thisBasePath.indexOf(',') !== -1) {
18734 thisBasePath = thisBasePath.replace(/\,/g,'+');
18735 }
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();
18743 });
18744 modules['dashboard'].widgetBeingEdited.widgetEle.find('.widgetPath').text(modules['dashboard'].widgetBeingEdited.displayName).attr('title','/r/'+thisBasePath);
18745 modules['dashboard'].updateWidget();
18746 }
18747 }
18748 );
18749 $(this.dashboardEditComponent).find('.cancelButton').click(
18750 function(e) {
18751 $('#editReddit').tokenInput('clear');
18752 $('#RESDashboardEditComponent').fadeOut(function() {
18753 $('#editReddit').blur();
18754 });
18755 }
18756 );
18757 $(document.body).append(this.dashboardEditComponent);
18758 },
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\//,'');
18767 var prepop = [];
18768 var reddits = basePath.split('+');
18769 for (var i=0, len=reddits.length; i<len; i++) {
18770 prepop.push({
18771 id: reddits[i],
18772 name: reddits[i]
18773 });
18774 }
18775 if (typeof modules['dashboard'].firstEdit === 'undefined') {
18776 $('#editReddit').tokenInput('/api/search_reddit_names.json?app=res', {
18777 method: "POST",
18778 queryParam: "query",
18779 theme: "facebook",
18780 allowFreeTagging: true,
18781 zindex: 999999999,
18782 onResult: function(response) {
18783 var names = response.names;
18784 var results = [];
18785 for (var i=0, len=names.length; i<len; i++) {
18786 results.push({id: names[i], name: names[i]});
18787 }
18788 if (names.length === 0) {
18789 var failedQueryValue = $('#token-input-editReddit').val();
18790 results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
18791 }
18792 return results;
18793 },
18794 onCachedResult: function(response) {
18795 var names = response.names;
18796 var results = [];
18797 for (var i=0, len=names.length; i<len; i++) {
18798 results.push({id: names[i], name: names[i]});
18799 }
18800 if (names.length === 0) {
18801 var failedQueryValue = $('#token-input-editReddit').val();
18802 results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
18803 }
18804 return results;
18805 },
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>"
18813 }
18814 });
18815 modules['dashboard'].firstEdit = true;
18816 } else {
18817 $('#editReddit').tokenInput('clear');
18818 for (var i=0, len=prepop.length; i<len; i++) {
18819 $('#editReddit').tokenInput('add', prepop[i]);
18820 }
18821 }
18822 },
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> \
18832 </div> \
18833 <div id="addMailWidgetContainer"> \
18834 <div class="backToWidgetTypes">&laquo; 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> \
18840 </div> \
18841 <div id="addUserFormContainer" class="addUserForm"> \
18842 <div class="backToWidgetTypes">&laquo; back</div> \
18843 <form id="addUserForm"><input type="text" id="addUser"><input type="submit" class="addButton" value="+add"></form> \
18844 </div> \
18845 <div id="addRedditFormContainer" class="addRedditForm"> \
18846 <div class="backToWidgetTypes">&laquo; 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> \
18848 </div> \
18849 ');
18850 $(this.dashboardAddComponent).find('.backToWidgetTypes').click(function(e) {
18851 $(this).parent().fadeOut(function() {
18852 $('#addWidgetButtons').fadeIn();
18853 });
18854 });
18855 $(this.dashboardAddComponent).find('.widgetShortcut').click(function(e) {
18856 var thisBasePath = $(this).attr('widgetPath');
18857 modules['dashboard'].addWidget({
18858 basePath: thisBasePath
18859 }, true);
18860 $('#addMailWidgetContainer').fadeOut(function() {
18861 $('#addWidgetButtons').fadeIn();
18862 });
18863 });
18864 $(this.dashboardAddComponent).find('#addRedditWidget').click(function(e) {
18865 $('#addWidgetButtons').fadeOut(function() {
18866 $('#addRedditFormContainer').fadeIn(function() {
18867 $('#token-input-addReddit').focus();
18868 });
18869 });
18870 });
18871 $(this.dashboardAddComponent).find('#addMailWidget').click(function(e) {
18872 $('#addWidgetButtons').fadeOut(function() {
18873 $('#addMailWidgetContainer').fadeIn();
18874 });
18875 });;
18876 $(this.dashboardAddComponent).find('#addUserWidget').click(function(e) {
18877 $('#addWidgetButtons').fadeOut(function() {
18878 $('#addUserFormContainer').fadeIn();
18879 });
18880 });;
18881 var thisEle = $(this.dashboardAddComponent).find('#addReddit');
18882 $(thisEle).tokenInput('/api/search_reddit_names.json?app=res', {
18883 method: "POST",
18884 queryParam: "query",
18885 theme: "facebook",
18886 allowFreeTagging: true,
18887 zindex: 999999999,
18888 onResult: function(response) {
18889 var names = response.names;
18890 var results = [];
18891 for (var i=0, len=names.length; i<len; i++) {
18892 results.push({id: names[i], name: names[i]});
18893 }
18894 if (names.length === 0) {
18895 var failedQueryValue = $('#token-input-addReddit').val();
18896 results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
18897 }
18898 return results;
18899 },
18900 onCachedResult: function(response) {
18901 var names = response.names;
18902 var results = [];
18903 for (var i=0, len=names.length; i<len; i++) {
18904 results.push({id: names[i], name: names[i]});
18905 }
18906 if (names.length === 0) {
18907 var failedQueryValue = $('#token-input-addReddit').val();
18908 results.push({id: failedQueryValue, name: failedQueryValue, failedResult: true});
18909 }
18910 return results;
18911 },
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>"
18919 }
18920 });
18921
18922 $(this.dashboardAddComponent).find('#addRedditForm').submit(
18923 function(e) {
18924 e.preventDefault();
18925 var thisBasePath = $('#addReddit').val();
18926 if (thisBasePath !== '') {
18927 if (thisBasePath.indexOf(',') !== -1) {
18928 thisBasePath = thisBasePath.replace(/\,/g,'+');
18929 }
18930 var thisDisplayName = ($('#addRedditDisplayName').val()) ? $('#addRedditDisplayName').val() : thisBasePath;
18931 modules['dashboard'].addWidget({
18932 basePath: thisBasePath,
18933 displayName: thisDisplayName
18934 }, true);
18935 // $('#addReddit').val('').blur();
18936 $('#addReddit').tokenInput('clear');
18937 $('#addRedditFormContainer').fadeOut(function() {
18938 $('#addReddit').blur();
18939 $('#addWidgetButtons').fadeIn();
18940 });
18941 }
18942 }
18943 );
18944 $(this.dashboardAddComponent).find('#addUserForm').submit(
18945 function(e) {
18946 e.preventDefault();
18947 var thisBasePath = '/user/'+$('#addUser').val();
18948 modules['dashboard'].addWidget({
18949 basePath: thisBasePath
18950 }, true);
18951 $('#addUser').val('').blur();
18952 $('#addUserFormContainer').fadeOut(function() {
18953 $('#addWidgetButtons').fadeIn();
18954 });
18955
18956 }
18957 );
18958 $(this.dashboardContents).append(this.dashboardAddComponent);
18959 this.dashboardUL = $('<ul id="RESDashboard"></ul>');
18960 $(this.dashboardContents).append(this.dashboardUL);
18961 },
18962 addWidget: function(optionsObject, isNew) {
18963 if (optionsObject.basePath.slice(0,1) !== '/') optionsObject.basePath = '/r/'+optionsObject.basePath;
18964 var exists=false;
18965 for (var i=0, len=this.widgets.length; i<len; i++) {
18966 if (this.widgets[i].basePath == optionsObject.basePath) {
18967 exists=true;
18968 break;
18969 }
18970 }
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();
18974 }, 1000);
18975 if (exists && isNew) {
18976 alert('A widget for '+optionsObject.basePath+' already exists!');
18977 } else {
18978 var thisWidget = new this.widgetObject(optionsObject);
18979 thisWidget.init();
18980 modules['dashboard'].saveWidget(thisWidget.optionsObject());
18981 }
18982 },
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) {
18988 exists = true;
18989 $('#'+modules['dashboard'].widgets[i].basePath.replace(/\/|\+/g,'_')).fadeOut('slow', function(ele) {
18990 $(this).detach();
18991 });
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();
18996 }, 1000);
18997 break;
18998 }
18999 }
19000 if (!exists) {
19001 RESUtils.notification({
19002 moduleID: 'dashboard',
19003 type: 'error',
19004 message: 'The widget you just tried to remove does not seem to exist.'
19005 });
19006 }
19007 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
19008 },
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) {
19014 exists = true;
19015 modules['dashboard'].widgets[i] = optionsObject;
19016 }
19017 }
19018 if (!exists) modules['dashboard'].widgets.push(optionsObject);
19019 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
19020 },
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) {
19026 exists = true;
19027 delete modules['dashboard'].widgetBeingEdited.formerBasePath;
19028 modules['dashboard'].widgets[i] = modules['dashboard'].widgetBeingEdited.optionsObject();
19029 }
19030 }
19031 RESStorage.setItem('RESmodules.dashboard.' + RESUtils.loggedInUser(), JSON.stringify(modules['dashboard'].widgets));
19032 },
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;
19038 }
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() {
19049 return {
19050 basePath: thisWidget.basePath,
19051 displayName: thisWidget.displayName,
19052 numPosts: thisWidget.numPosts,
19053 sortBy: thisWidget.sortBy,
19054 minimized: thisWidget.minimized
19055 }
19056 }
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'));
19061 });
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();
19066 }, 100);
19067 }
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">&times;</li></ul>');
19069 $(thisWidget.stateControls).find('li').click(function (e) {
19070 switch ($(e.target).attr('action')) {
19071 case 'refresh':
19072 thisWidget.update();
19073 break;
19074 case 'refreshAll':
19075 $('li[action="refresh"]').click();
19076 break;
19077 case 'addRow':
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();
19084 break;
19085 case 'subRow':
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();
19092 break;
19093 case 'minimize':
19094 $(thisWidget.widgetEle).toggleClass('minimized');
19095 if ($(thisWidget.widgetEle).hasClass('minimized')) {
19096 $(e.target).text('+');
19097 thisWidget.minimized = true;
19098 } else {
19099 $(e.target).text('-');
19100 thisWidget.minimized = false;
19101 thisWidget.update();
19102 }
19103 $(thisWidget.contents).parent().slideToggle();
19104 modules['dashboard'].saveWidget(thisWidget.optionsObject());
19105 break;
19106 case 'delete':
19107 modules['dashboard'].removeWidget(thisWidget.optionsObject());
19108 break;
19109 }
19110 });
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());
19118 }
19119 thisWidget.edit = function(e) {
19120 modules['dashboard'].widgetBeingEdited = thisWidget;
19121 modules['dashboard'].showEditForm();
19122 }
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+'/';
19129 } else {
19130 thisWidget.sortPath = '';
19131 }
19132 thisWidget.url = location.protocol + '//' + location.hostname + '/' + thisWidget.basePath + thisWidget.sortPath;
19133 $(thisWidget.contents).fadeTo('fast',0.25);
19134 $(thisWidget.scrim).fadeIn();
19135 $.ajax({
19136 url: thisWidget.url,
19137 data: {
19138 limit: thisWidget.numPosts
19139 },
19140 success: thisWidget.populate,
19141 error: thisWidget.error
19142 });
19143 }
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('+');
19148 }
19149 thisWidget.scrim = $(thisWidget.widgetEle).find('.RESDashboardComponentScrim');
19150 thisWidget.contents = $(thisWidget.container).find('.RESDashboardComponentContents');
19151 thisWidget.init = function() {
19152 if (RESUtils.currentSubreddit('dashboard')) {
19153 thisWidget.draw();
19154 if (!thisWidget.minimized) modules['dashboard'].addToUpdateQueue(thisWidget.update);
19155 }
19156 }
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();
19163 }
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
19175 })
19176 $(thisWidget.stateControls).find('.updateTime').text('updated: '+RESUtils.niceDateTime());
19177 } else {
19178 if (thisWidget.url.indexOf('/message/') !== -1) {
19179 $(thisWidget.contents).html('<div class="widgetNoMail">No messages were found.</div>');
19180 } else {
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>');
19182 }
19183 $(thisWidget.contents).fadeTo('fast',1);
19184 $(thisWidget.scrim).fadeOut();
19185 $(thisWidget.stateControls).find('.updateTime').text('updated: '+RESUtils.niceDateTime());
19186 }
19187 // now run watcher functions from other modules on this content...
19188 RESUtils.watchers.siteTable.forEach(function(callback) {
19189 if (callback) callback(widgetContent[0]);
19190 });
19191
19192 }
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>');
19198 } else {
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>');
19200 }
19201 $(thisWidget.scrim).fadeOut();
19202 $(thisWidget.contents).fadeTo('fast',1);
19203 }
19204 },
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();
19215 } else {
19216 var isMulti = true;
19217 var thisSubredditFragment = $(subButton).next().text();
19218 }
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...)
19223 if (isMulti) {
19224 var theWrap = $(subButton).parent();
19225 $(theWrap).appendTo($(theWrap).parent());
19226 }
19227 }
19228 var dashboardToggle = document.createElement('span');
19229 dashboardToggle.setAttribute('class','REStoggle RESDashboardToggle');
19230 dashboardToggle.setAttribute('data-subreddit',thisSubredditFragment);
19231 var exists=false;
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())) {
19234 exists=true;
19235 break;
19236 }
19237 }
19238 if (exists) {
19239 dashboardToggle.textContent = '-dashboard';
19240 dashboardToggle.setAttribute('title','Remove this subreddit from your dashboard');
19241 dashboardToggle.classList.add('remove');
19242 } else {
19243 dashboardToggle.textContent = '+dashboard';
19244 dashboardToggle.setAttribute('title','Add this subreddit to your dashboard');
19245 }
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');
19253 }
19254 }
19255 },
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
19261 }, true);
19262 e.target.textContent = '+dashboard';
19263 e.target.classList.remove('remove');
19264 } else {
19265 modules['dashboard'].addWidget({
19266 basePath: thisBasePath
19267 }, true);
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>'
19273 });
19274 e.target.classList.add('remove');
19275 }
19276 },
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();
19286 });
19287 }
19288 };
19289
19290 modules['subredditInfo'] = {
19291 moduleID: 'subredditInfo',
19292 moduleName: 'Subreddit Info',
19293 category: 'UI',
19294 options: {
19295 hoverDelay: {
19296 type: 'text',
19297 value: 800,
19298 description: 'Delay, in milliseconds, before hover tooltip loads. Default is 800.'
19299 },
19300 fadeDelay: {
19301 type: 'text',
19302 value: 200,
19303 description: 'Delay, in milliseconds, before hover tooltip fades away. Default is 200.'
19304 },
19305 fadeSpeed: {
19306 type: 'text',
19307 value: 0.3,
19308 description: 'Fade animation\'s speed. Default is 0.3, the range is 0-1. Setting the speed to 1 will disable the animation.'
19309 },
19310 USDateFormat: {
19311 type: 'boolean',
19312 value: false,
19313 description: 'Show date (subreddit created...) in US format (i.e. 08-31-2010)'
19314 }
19315 },
19316 description: 'Adds a hover tooltip to subreddits',
19317 isEnabled: function() {
19318 return RESConsole.getModulePrefs(this.moduleID);
19319 },
19320 include: [
19321 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
19322 ],
19323 isMatchURL: function() {
19324 return RESUtils.isMatchURL(this.moduleID);
19325 },
19326 beforeLoad: function() {
19327 if ((this.isEnabled()) && (this.isMatchURL())) {
19328 var css = '';
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);
19334 }
19335 },
19336 go: function() {
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;
19341
19342 // get subreddit links and add event listeners...
19343 this.addListeners();
19344 RESUtils.watchForElement('siteTable', modules['subredditInfo'].addListeners);
19345 }
19346 },
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, {
19357 width: 450,
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, {});
19362 }, false);
19363 }
19364 }
19365 }
19366 },
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 />');
19375 subscribeToggle
19376 .attr('id', 'RESHoverInfoSubscriptionButton')
19377 .addClass('RESFilterToggle')
19378 .css('margin-left', '12px')
19379 .hide()
19380 .on('click', modules['subredditInfo'].toggleSubscription);
19381 modules['subredditInfo'].updateToggleButton(subscribeToggle, false);
19382
19383 header.appendChild(subscribeToggle[0]);
19384 }
19385 var body = '\
19386 <div class="subredditInfoToolTip">\
19387 <a class="hoverSubreddit" href="/user/'+escapeHTML(thisSubreddit)+'">'+escapeHTML(thisSubreddit)+'</a>:<br>\
19388 <img src="'+RESConsole.loader+'"> loading...\
19389 </div>';
19390 def.notify(header, null);
19391 if (typeof mod.subredditInfoCache[thisSubreddit] !== 'undefined') {
19392 mod.writeSubredditInfo(mod.subredditInfoCache[thisSubreddit], def);
19393 } else {
19394 GM_xmlhttpRequest({
19395 method: "GET",
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);
19402 } else {
19403 mod.writeSubredditInfo({}, def);
19404 }
19405 }
19406 });
19407 }
19408 },
19409 updateCache: function(subreddit, data) {
19410 subreddit = subreddit.toLowerCase();
19411 if (!data.data) {
19412 data = { data : data };
19413 }
19414 this.subredditInfoCache = this.subredditInfoCache || [];
19415 this.subredditInfoCache[subreddit] = $.extend(true, {}, this.subredditInfoCache[subreddit], data);
19416 },
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)
19422 return;
19423 }
19424 var utctime = jsonData.data.created_utc;
19425 var d = new Date(utctime * 1000);
19426 var isOver18;
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>';
19437
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());
19445 var idx = -1;
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()) {
19448 idx=i;
19449 break;
19450 }
19451 }
19452 if (idx !== -1) {
19453 theSC.textContent = '-shortcut';
19454 theSC.setAttribute('title','Remove this subreddit from your shortcut bar');
19455 theSC.classList.add('remove');
19456 } else {
19457 theSC.textContent = '+shortcut';
19458 theSC.setAttribute('title','Add this subreddit to your shortcut bar');
19459 }
19460 theSC.addEventListener('click', modules['subredditManager'].toggleSubredditShortcut, false);
19461
19462 newBody.find('#subTooltipButtons').append(theSC);
19463 }
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());
19468 var exists=false;
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())) {
19471 exists=true;
19472 break;
19473 }
19474 }
19475 if (exists) {
19476 dashboardToggle.textContent = '-dashboard';
19477 dashboardToggle.setAttribute('title','Remove this subreddit from your dashboard');
19478 dashboardToggle.classList.add('remove');
19479 } else {
19480 dashboardToggle.textContent = '+dashboard';
19481 dashboardToggle.setAttribute('title','Add this subreddit to your dashboard');
19482 }
19483 dashboardToggle.addEventListener('click', modules['dashboard'].toggleDashboard, false);
19484 newBody.find('#subTooltipButtons').append(dashboardToggle);
19485 }
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());
19490 var exists=false;
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())) {
19494 exists=true;
19495 break;
19496 }
19497 }
19498 if (exists) {
19499 filterToggle.textContent = '-filter';
19500 filterToggle.setAttribute('title','Stop filtering from /r/all and /domain/*');
19501 filterToggle.classList.add('remove');
19502 } else {
19503 filterToggle.textContent = '+filter';
19504 filterToggle.setAttribute('title','Filter this subreddit from /r/all and /domain/*');
19505 }
19506 filterToggle.addEventListener('click', modules['filteReddit'].toggleFilter, false);
19507 newBody.find('#subTooltipButtons').append(filterToggle);
19508 }
19509
19510 if (RESUtils.loggedInUser()) {
19511 var subscribed = !!jsonData.data.user_is_subscriber;
19512
19513 var subscribeToggle = $('#RESHoverInfoSubscriptionButton');
19514 subscribeToggle.attr('data-subreddit',jsonData.data.display_name.toLowerCase());
19515 modules['subredditInfo'].updateToggleButton(subscribeToggle, subscribed);
19516 subscribeToggle.fadeIn('fast');
19517 }
19518
19519 deferred.resolve(null, newBody)
19520 },
19521 updateToggleButton: function(toggleButton, subscribed) {
19522 if (toggleButton instanceof jQuery) toggleButton = toggleButton[0];
19523 var toggleOn = '+subscribe';
19524 var toggleOff = '-unsubscribe';
19525 if (subscribed) {
19526 toggleButton.textContent = toggleOff;
19527 toggleButton.classList.add('remove');
19528 } else {
19529 toggleButton.textContent = toggleOn;
19530 toggleButton.classList.remove('remove');
19531 }
19532 },
19533 toggleSubscription: function(e) {
19534 // Get info
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;
19539
19540 modules['subredditInfo'].updateToggleButton(subscribeToggle, subscribing);
19541
19542 modules['subredditManager'].subscribeToSubreddit(subredditData.name, subscribing);
19543 modules['subredditInfo'].updateCache(subreddit, { 'user_is_subscriber': subscribing });
19544 }
19545 }; // note: you NEED this semicolon at the end!
19546
19547
19548
19549 /**
19550 * CommentHidePersistor - stores hidden comments in localStorage and re-hides
19551 * them on reload of the page.
19552 **/
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: {},
19559 hiddenKeys: [],
19560 hiddenThings: [],
19561 hiddenThingsKey: window.location.href,
19562 maxKeys: 100,
19563 pruneKeysTo: 50,
19564
19565 options: {},
19566 isEnabled: function () {
19567 return RESConsole.getModulePrefs(this.moduleID);
19568 },
19569 include: [
19570 /^https?:\/\/([a-z]+)\.reddit\.com\/[-\w\.\/]+\/comments\/[-\w\.]+/i,
19571 /^https?:\/\/([a-z]+)\.reddit\.com\/comments\/[-\w\.]+/i
19572 ],
19573 isMatchURL: function () {
19574 return RESUtils.isMatchURL(this.moduleID);
19575 },
19576 go: function () {
19577 if ((this.isEnabled()) && (this.isMatchURL())) {
19578 m_chp.bindToHideLinks();
19579 m_chp.hideHiddenThings();
19580 }
19581 },
19582 bindToHideLinks: function () {
19583 /**
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.
19586 **/
19587 $('body').on('click', 'a.expand', function () {
19588 var thing = $(this).parents('.thing'),
19589 thingId = thing.data('fullname'),
19590 collapsing = !$(this).parent().is('.collapsed');
19591
19592 /* Add our key to pages interacted with, for potential pruning
19593 later */
19594 if (m_chp.hiddenKeys.indexOf(m_chp.hiddenThingsKey) === -1) {
19595 m_chp.hiddenKeys.push(m_chp.hiddenThingsKey);
19596 }
19597
19598 if (collapsing) {
19599 m_chp.addHiddenThing(thingId);
19600 } else {
19601 m_chp.removeHiddenThing(thingId);
19602 }
19603 });
19604 },
19605 loadHiddenThings: function () {
19606 var hidePersistorJson = RESStorage.getItem('RESmodules.commentHidePersistor.hidePersistor')
19607
19608 if (hidePersistorJson) {
19609 try {
19610 m_chp.hidePersistorData = safeJSON.parse(hidePersistorJson)
19611 m_chp.allHiddenThings = m_chp.hidePersistorData['hiddenThings']
19612 m_chp.hiddenKeys = m_chp.hidePersistorData['hiddenKeys']
19613
19614 /**
19615 * Prune allHiddenThings of old content so it doesn't get
19616 * huge.
19617 **/
19618 if (m_chp.hiddenKeys.length > m_chp.maxKeys) {
19619 var pruneStart = m_chp.maxKeys - m_chp.pruneKeysTo,
19620 newHiddenThings = {},
19621 newHiddenKeys = [];
19622
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];
19628 }
19629 m_chp.allHiddenThings = newHiddenThings;
19630 m_chp.hiddenKeys = newHiddenKeys;
19631 m_chp.syncHiddenThings();
19632 }
19633
19634 if (typeof m_chp.allHiddenThings[m_chp.hiddenThingsKey] !== 'undefined') {
19635 m_chp.hiddenThings = m_chp.allHiddenThings[m_chp.hiddenThingsKey];
19636 return;
19637 }
19638 } catch(e) {}
19639 }
19640 },
19641 addHiddenThing: function (thingId) {
19642 var i = m_chp.hiddenThings.indexOf(thingId);
19643 if (i === -1) {
19644 m_chp.hiddenThings.push(thingId);
19645 }
19646 m_chp.syncHiddenThings();
19647 },
19648 removeHiddenThing: function (thingId) {
19649 var i = m_chp.hiddenThings.indexOf(thingId);
19650 if (i !== -1) {
19651 m_chp.hiddenThings.splice(i, 1);
19652 }
19653 m_chp.syncHiddenThings();
19654 },
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
19661 }
19662 RESStorage.setItem('RESmodules.commentHidePersistor.hidePersistor', JSON.stringify(hidePersistorData));
19663 },
19664 hideHiddenThings: function () {
19665 m_chp.loadHiddenThings();
19666
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');
19672 if ($hideLink) {
19673 /**
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.
19678 **/
19679 (function ($hideLink) {
19680 window.setTimeout(function () {
19681 // $hideLink.click();
19682 RESUtils.click($hideLink);
19683 }, 0);
19684 })($hideLink)
19685 }
19686 }
19687 }
19688 };
19689
19690
19691
19692
19693 modules['bitcointip'] = {
19694 moduleID: 'bitcointip',
19695 moduleName: 'bitcointip',
19696 category: 'Users',
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>.',
19704 options: {
19705 baseTip: {
19706 name: 'Default Tip',
19707 type: 'text',
19708 value: '0.01 BTC',
19709 description: 'Default tip amount in the form of ' +
19710 '"[value] [units]", e.g. "0.01 BTC"'
19711 },
19712 attachButtons: {
19713 name: 'Add "tip bitcoins" Button',
19714 type: 'boolean',
19715 value: true,
19716 description: 'Attach "tip bitcoins" button to comments'
19717 },
19718 hide: {
19719 name: 'Hide Bot Verifications',
19720 type: 'boolean',
19721 value: true,
19722 description: 'Hide bot verifications'
19723 },
19724 status: {
19725 name: 'Tip Status Format',
19726 type: 'enum',
19727 values: [
19728 { name: 'detailed', value: 'detailed' },
19729 { name: 'basic', value: 'basic' },
19730 { name: 'none', value: 'none' }
19731 ],
19732 value: 'detailed',
19733 description: 'Tip status - level of detail'
19734 },
19735 currency: {
19736 name: 'Preferred Currency',
19737 type: 'enum',
19738 values: [
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' }
19744 ],
19745 value: 'USD',
19746 description: 'Preferred currency units'
19747 },
19748 balance: {
19749 name: 'Display Balance',
19750 type: 'boolean',
19751 value: true,
19752 description: 'Display balance'
19753 },
19754 subreddit: {
19755 name: 'Display Enabled Subreddits',
19756 type: 'boolean',
19757 value: false,
19758 description: 'Display enabled subreddits'
19759 },
19760 address: {
19761 name: 'Known User Addresses',
19762 type: 'table',
19763 addRowText: '+add address',
19764 fields: [
19765 {name: 'user', type: 'text'},
19766 {name: 'address', type: 'text'}
19767 ],
19768 value: [
19769 /* ['skeeto', '1...'] */
19770 ],
19771 description: 'Mapping of usernames to bitcoin addresses'
19772 },
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>",
19778 type: 'button',
19779 callback: null // populated when module loads
19780 }
19781 },
19782 isEnabled: function() {
19783 return RESConsole.getModulePrefs(this.moduleID);
19784 },
19785 include: [
19786 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
19787 ],
19788 exclude: [
19789 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*\/user\/bitcointip\/?/i
19790 ],
19791 isMatchURL: function() {
19792 return RESUtils.isMatchURL(this.moduleID);
19793 },
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; }');
19801 },
19802
19803 go: function() {
19804 if (!this.isEnabled() || !this.isMatchURL()) {
19805 return;
19806 }
19807
19808 if (this.options.status.value === 'basic') {
19809 this.icons.pending = this.icons.completed;
19810 this.icons.reversed = this.icons.completed;
19811 }
19812
19813 if (this.options.subreddit.value) {
19814 this.attachSubredditIndicator();
19815 }
19816
19817 if (this.options.balance.value) {
19818 this.attachBalance();
19819 }
19820
19821 if (RESUtils.currentSubreddit() === 'bitcointip') {
19822 this.injectBotStatus();
19823 }
19824
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();
19830 }
19831
19832 if (this.options.hide.value) {
19833 this.hideVerifications();
19834 RESUtils.watchForElement('newComments', modules['bitcointip'].hideVerifications.bind(this));
19835 }
19836
19837
19838 if (this.options.status.value !== 'none') {
19839 this.scanForTips();
19840 RESUtils.watchForElement('newComments', this.scanForTips.bind(this));
19841 }
19842 }
19843 },
19844
19845 save: function save() {
19846 var json = JSON.stringify(this.options);
19847 RESStorage.setItem('RESoptions.bitcoinTip', json);
19848 },
19849
19850 load: function load() {
19851 var json = RESStorage.getItem('RESoptions.bitcoinTip');
19852 if (json) {
19853 this.options = JSON.parse(json);
19854 }
19855 },
19856
19857
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,
19861
19862 /** How many milliseconds until the bot is considered down. */
19863 botDownThreshold: 15 * 60 * 1000,
19864
19865 /** Bitcointip API endpoints. */
19866 api: {
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'
19871 },
19872
19873 /** Encoded tipping icons. */
19874 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="
19880 },
19881
19882 /** Specifies how to display different currencies. */
19883 currencies: {
19884 USD: {unit: 'US$', precision: 2},
19885 BTC: {unit: '฿'},
19886 JPY: {unit: 'Â¥'},
19887 GBP: {unit: '£', precision: 2},
19888 EUR: {unit: '€', precision: 2},
19889 AUD: {unit: 'A$', precision: 2},
19890 CAD: {unit: 'C$', precision: 2}
19891 },
19892
19893 /** Return a DOM element to separate items in the user bar. */
19894 separator: function() {
19895 return $('<span>|</span>').addClass('separator');
19896 },
19897
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'];
19906 }
19907 if (unit.precision) {
19908 amount = parseFloat(amount).toFixed(unit.precision);
19909 }
19910 return unit.unit + amount;
19911 },
19912
19913 tipPublicly: function tipPublicly($target) {
19914 var form = null;
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');
19921 }
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);
19926 }
19927 },
19928
19929 tipPrivately: function tipPrivately($target) {
19930 var form = null;
19931 if ($target.closest('.link').length > 0) { /* Post */
19932 form = $('.commentarea .usertext:first');
19933 } else {
19934 form = $target.closest('.thing').find(".child .usertext:first");
19935 }
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?')) {
19939 return;
19940 }
19941 }
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;
19946 },
19947
19948 attachTipButtons: function attachTipButtons(ele) {
19949 ele = ele || document.body;
19950 var module = this;
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>' +
19956 '</div>' +
19957 '</span>');
19958 module.tipButton.bind('click', function(e) {
19959 modules['bitcointip'].toggleTipMenu(e.target);
19960 });
19961 }
19962
19963
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)));
19969 });
19970
19971 if (!module.attachedPostTipButton) {
19972 module.attachedPostTipButton = true; // signifies either "attached button" or "decided not to attach button"
19973
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)));
19978 }
19979 }
19980
19981 },
19982
19983 attachTipMenu: function() {
19984 this.tipMenu =
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>' +
19988 '</div>');
19989
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')
19994 );
19995 }
19996 $(document.body).append(this.tipMenu);
19997
19998 this.tipMenu.find('a').click(function(event) {
19999 modules['bitcointip'].toggleTipMenu();
20000 });
20001
20002 this.tipMenu.find('.tip-publicly').click(function(event) {
20003 event.preventDefault();
20004 modules['bitcointip'].tipPublicly($(modules['bitcointip'].lastToggle));
20005 });
20006
20007 this.tipMenu.find('.tip-privately').click(function(event) {
20008 event.preventDefault();
20009 modules['bitcointip'].tipPrivately($(modules['bitcointip'].lastToggle));
20010 });
20011 },
20012
20013
20014 toggleTipMenu: function(ele) {
20015 var tipMenu = modules['bitcointip'].tipMenu;
20016
20017 if (!ele || ele.length === 0) {
20018 tipMenu.hide();
20019 return;
20020 }
20021
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)) {
20026 tipMenu.hide();
20027 }
20028 tipMenu.css({
20029 top: (thisXY.top+thisHeight)+'px',
20030 left: thisXY.left+'px'
20031 });
20032 tipMenu.toggle();
20033 modules['bitcointip'].lastToggle = ele;
20034 },
20035
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.'
20048 }));
20049 }
20050 }.bind(this));
20051 }
20052 },
20053
20054 hideVerifications: function hideVerifications(ele) {
20055 ele = ele || document.body;
20056
20057 /* t2_7vw3n is u/bitcointip. */
20058
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;
20064
20065 var hasReplies = $this.find('.comment').length > 0;
20066 if (hasReplies) return;
20067
20068 $this.find('.expand').eq(2).click();
20069 });
20070 },
20071
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];
20076 this.save();
20077 },
20078
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];
20084 });
20085 return address;
20086 },
20087
20088 setAddress: function setAddress(user, address) {
20089 user = user || RESUtils.loggedInUser();
20090 var set = false;
20091 this.options.address.value.forEach(function(row) {
20092 if (row[0] === user) {
20093 row[1] = address;
20094 set = true;
20095 }
20096 });
20097 if (user && !set) {
20098 this.options.address.value.push([user, address]);
20099 }
20100 this.save();
20101 return address;
20102 },
20103
20104 attachBalance: function attachBalance() {
20105 var user = RESUtils.loggedInUser();
20106 var address = this.getAddress(user);
20107 if (!address) return;
20108 var bitcointip = this;
20109
20110 $.getJSON(this.api.balance, {
20111 username: user,
20112 address: address
20113 }, function (balance) {
20114 if (!('balanceBTC' in balance)) {
20115 return; /* Probably have the address wrong! */
20116 }
20117 $('#header-bottom-right form.logout')
20118 .before(bitcointip.separator()).prev()
20119 .before($('<a/>').attr({
20120 'class': 'hover',
20121 'href': '#'
20122 }).click(function() {
20123 bitcointip.toggleCurrency();
20124 $(this).text(bitcointip.quantityString(balance));
20125 }).text(bitcointip.quantityString(balance)));
20126 });
20127 },
20128
20129 fetchAddressForCurrentUser: function () {
20130 var user = RESUtils.loggedInUser();
20131 if (!user) {
20132 RESUtils.notification({
20133 moduleID: 'bitcointip',
20134 optionKey: 'fetchWalletAddress',
20135 type: 'error',
20136 message: 'Log in, then try again.'
20137 });
20138 return;
20139 }
20140 this.fetchAddress(user, function(address) {
20141 if (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.'
20148 });
20149 } else {
20150 RESUtils.notification({
20151 moduleID: 'bitcointip',
20152 type: 'error',
20153 message: 'Could not find address for user ' + user
20154 });
20155 }
20156
20157 });
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.'
20163 });
20164 },
20165
20166 fetchAddress: function fetchAddress(user, callback) {
20167 user = user || RESUtils.loggedInUser();
20168 callback = callback || function nop() {};
20169 if (!user) return;
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);
20177 if (address) {
20178 return address[1];
20179 } else {
20180 return false;
20181 }
20182 }).filter(function(x) { return x; })[0]; // Use the most recent
20183 if (address) {
20184 this.setAddress(user, address);
20185 callback(address);
20186 } else {
20187 callback(null);
20188 }
20189 }.bind(this));
20190 },
20191
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));
20200 }
20201 },
20202
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());
20207 });
20208 },
20209
20210 /** Find all things matching a regex. */
20211 getTips: function getComments(regex, ele) {
20212 var tips = {};
20213 var items = $(ele);
20214 if (items.is('.entry')) {
20215 items = items.closest('div.comment, div.self, div.link');
20216 } else {
20217 items = items.find('div.comment, div.self, div.link');
20218 }
20219 var module = this;
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;
20225 }
20226 });
20227 return tips;
20228 },
20229
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],
20244 style: iconStyle,
20245 title: this.quantityString(tip) + ' → ' + tip.receiver +
20246 ' (' + tip.status + ')'
20247 })));
20248 tips[id].attr('id', 't1_' + id); // for later linking
20249 delete tips[id];
20250 }.bind(this));
20251
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
20256 }
20257 var date = tips[id].find('.tagline time:first')
20258 .attr('datetime');
20259 if (new Date(date) < lastEvaluated) {
20260 var tagline = tips[id].find('.tagline:first');
20261 tagline.append($('<img/>').attr({
20262 src: icons.cancelled,
20263 style: iconStyle,
20264 title: 'This tip is invalid.'
20265 }));
20266 }
20267 }
20268 }.bind(this));
20269 },
20270
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 ';
20285 if (plural) {
20286 title = 'redditors have given ' + title;
20287 } else {
20288 title = 'a redditor has given ' + title;
20289 }
20290 if (thing.closest('.link').length === 0) {
20291 title += 'comment.';
20292 } else {
20293 title += 'submission.';
20294 }
20295 var icon = $('<img/>').attr({
20296 src: icons.tipped,
20297 style: iconStyle,
20298 title: title
20299 });
20300 tagline.append(icon);
20301 if (plural) {
20302 tagline.append($('<span/>').text('x' + tipped.tipQTY));
20303 }
20304 }.bind(this));
20305 }.bind(this));
20306 },
20307
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>';
20314 } else {
20315 botStatus = '<span class="status-up">UP</span>';
20316 }
20317 $('.side a[href="http://bitcointip.net/status.php"]').html(botStatus);
20318 });
20319 }
20320 };
20321
20322 modules['troubleShooter'] = {
20323 moduleID: 'troubleShooter',
20324 moduleName: 'Troubleshooter',
20325 category: 'Troubleshoot',
20326 options: {
20327 clearUserInfoCache: {
20328 type: 'button',
20329 text: 'Clear',
20330 callback: null,
20331 description: 'Reset the <code>userInfo</code> cache for the currently logged in user. Useful for when link/comment karma appears to have frozen.'
20332 },
20333 clearSubreddits: {
20334 type: 'button',
20335 text: 'Clear',
20336 callback: null,
20337 description: 'Reset the \'My Subreddits\' dropdown contents in the event of old/duplicate/missing entries.'
20338 },
20339 clearTags: {
20340 type: 'button',
20341 text: 'Clear',
20342 callback: null,
20343 description: 'Remove all entries for users with +1 or -1 vote tallies (only non-tagged users).'
20344 },
20345 resetToFactory: {
20346 type: 'button',
20347 text: 'Reset',
20348 callback: null,
20349 description: 'Warning: This will remove all your RES settings, including tags, saved comments, filters etc!'
20350 }
20351 },
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);
20359 },
20360 include: [
20361 /^https?:\/\/([a-z]+)\.reddit\.com\/[\?]*/i
20362 ],
20363 isMatchURL: function () {
20364 return RESUtils.isMatchURL(this.moduleID);
20365 },
20366 beforeLoad: function () {
20367 var css = '';
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);
20371 },
20372 go: function () {
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;
20377 },
20378 clearUICache: function () {
20379 var user = RESUtils.loggedInUser();
20380 if (user) {
20381 RESStorage.removeItem('RESUtils.userInfoCache.' + user);
20382 RESUtils.notification('Cached info for ' + user + ' was reset.', 2500);
20383 } else {
20384 RESUtils.notification('You must be logged in to perform this task.', 2500);
20385 }
20386 },
20387 clearSubreddits: function () {
20388 var user = RESUtils.loggedInUser();
20389 if (user) {
20390 RESStorage.removeItem('RESmodules.subredditManager.subreddits.' + user);
20391 RESUtils.notification('Subreddits for ' + user + ' were reset.', 2500);
20392 } else {
20393 RESUtils.notification('You must be logged in to perform this task.', 2500);
20394 }
20395 },
20396 clearTags: function () {
20397 var confirm = window.confirm('Are you positive?');
20398 if (confirm) {
20399 var i,
20400 cnt = 0,
20401 tags = RESStorage.getItem('RESmodules.userTagger.tags');
20402 if (tags) {
20403 tags = JSON.parse(tags);
20404 for (i in tags) {
20405 if ((tags[i].votes === 1 || tags[i].votes === -1) && !tags[i].hasOwnProperty('tag')) {
20406 delete tags[i];
20407 cnt += 1;
20408 }
20409 }
20410 tags = JSON.stringify(tags);
20411 RESStorage.setItem('RESmodules.userTagger.tags', tags);
20412 RESUtils.notification(cnt + ' entries removed.', 2500);
20413 }
20414 } else {
20415 RESUtils.notification('No action was taken', 2500);
20416 }
20417 },
20418 resetToFactory: function () {
20419 var confirm = window.confirm('This will kill all your settings and saved data. Are you sure?');
20420 if (confirm) {
20421 for (var key in RESStorage) {
20422 if (key.indexOf('RES') !== -1) {
20423 RESStorage.removeItem(key);
20424 }
20425 }
20426 RESUtils.notification('All settings reset.', 2500);
20427 } else {
20428 RESUtils.notification('No action was taken', 2500);
20429 }
20430 }
20431 };
20432
20433
20434 /* END MODULES */
20435
20436 /*
20437 * Konami-JS ~
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
20447 */
20448 var Konami = function () {
20449 var konami = {
20450 addEvent: function (obj, type, fn, ref_obj) {
20451 if (obj.addEventListener)
20452 obj.addEventListener(type, fn, false);
20453 else if (obj.attachEvent) {
20454 // IE
20455 obj["e" + type + fn] = fn;
20456 obj[type + fn] = function () {
20457 obj["e" + type + fn](window.event, ref_obj);
20458 }
20459
20460 obj.attachEvent("on" + type, obj[type + fn]);
20461 }
20462 },
20463 input: "",
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) {
20473 konami.code(link);
20474 konami.input = "";
20475 return;
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;
20480 }, 2000);
20481 }
20482 }, this);
20483 this.iphone.load(link);
20484 },
20485 code: function (link) {
20486 window.location = link;
20487 },
20488 iphone: {
20489 start_x: 0,
20490 start_y: 0,
20491 stop_x: 0,
20492 stop_y: 0,
20493 tap: false,
20494 capture: false,
20495 orig_keys: "",
20496 keys: ["UP", "UP", "DOWN", "DOWN", "LEFT", "RIGHT", "LEFT", "RIGHT", "TAP", "TAP", "TAP"],
20497 code: function (link) {
20498 konami.code(link);
20499 },
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();
20510 }
20511 });
20512 konami.addEvent(document, "touchend", function (evt) {
20513 if (konami.iphone.tap == true) konami.iphone.check_direction(link);
20514 }, false);
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;
20520 });
20521 },
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;
20529
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;
20533 this.code(link);
20534 }
20535 }
20536 }
20537 }
20538 return konami;
20539 };
20540
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);
20551 }
20552 }
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();
20557 }
20558 }
20559 // apply style...
20560 GM_addStyle(RESUtils.css);
20561 // clear out css cache...
20562 RESUtils.css = '';
20563 }
20564
20565 function RESInit() {
20566 // $.browser shim since jQuery removed it
20567 $.browser = {
20568 safari: BrowserDetect.isSafari(),
20569 mozilla: BrowserDetect.isFirefox(),
20570 chrome: BrowserDetect.isChrome(),
20571 opera: BrowserDetect.isOpera()
20572 };
20573 $.fn.safeHtml = function(string) {
20574 if (!string) return '';
20575 else return $(this).html(RESUtils.sanitizeHTML(string));
20576 }
20577
20578 RESUtils.initObservers();
20579 localStorageFail = false;
20580 /*
20581 var backup = {};
20582 $.extend(backup, RESStorage);
20583 delete backup.getItem;
20584 delete backup.setItem;
20585 delete backup.removeItem;
20586 console.log(backup);
20587 */
20588
20589 // Check for localStorage functionality...
20590 try {
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');
20599
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)) {
20607 GMSVtoFFSS();
20608 }
20609 }, true);
20610 }
20611 }
20612 } catch(e) {
20613 localStorageFail = true;
20614 }
20615
20616 document.body.classList.add('res','res-v430');
20617
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".';
20626 } else {
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)".';
20628 }
20629 var userMenu = document.querySelector('#header-bottom-right');
20630 if (userMenu) {
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();
20639 alert(RESFail);
20640 }, true);
20641 RESPrefsLink.textContent = '[RES - ERROR]';
20642 RESPrefsLink.setAttribute('style','color: red; font-weight: bold;');
20643 insertAfter(preferencesUL, RESPrefsLink);
20644 insertAfter(preferencesUL, separator);
20645 }
20646 } else {
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());
20665 }
20666 }
20667 GM_addStyle(RESUtils.css);
20668 // console.log('end: ' + Date());
20669 }
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');
20676 }
20677 }
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');
20685 }, 500);
20686 }
20687 konami.load();
20688
20689 }
20690
20691 RESUtils.postLoad = true;
20692 }
20693
20694 RESStorage = {};
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];
20701 return null;
20702 }
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;
20710 var thisJSON = {
20711 requestType: 'localStorage',
20712 operation: 'setItem',
20713 itemName: key,
20714 itemValue: value
20715 };
20716 if (!fromBG) {
20717 chrome.extension.sendMessage(thisJSON);
20718 }
20719 }
20720 }
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];
20725 var thisJSON = {
20726 requestType: 'localStorage',
20727 operation: 'removeItem',
20728 itemName: key
20729 };
20730 chrome.extension.sendMessage(thisJSON);
20731 }
20732 window.localStorage = RESStorage;
20733 //RESInit();
20734 } else if (BrowserDetect.isSafari()) {
20735 RESStorage = response;
20736 RESStorage.getItem = function(key) {
20737 if (typeof RESStorage[key] !== 'undefined') return RESStorage[key];
20738 return null;
20739 }
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;
20746 var thisJSON = {
20747 requestType: 'localStorage',
20748 operation: 'setItem',
20749 itemName: key,
20750 itemValue: value
20751 }
20752 if (!fromBG) {
20753 safari.self.tab.dispatchMessage("localStorage", thisJSON);
20754 }
20755 }
20756 }
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];
20761 var thisJSON = {
20762 requestType: 'localStorage',
20763 operation: 'removeItem',
20764 itemName: key
20765 }
20766 safari.self.tab.dispatchMessage("localStorage", thisJSON);
20767 }
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];
20773 return null;
20774 }
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;
20781 var thisJSON = {
20782 requestType: 'localStorage',
20783 operation: 'setItem',
20784 itemName: key,
20785 itemValue: value
20786 }
20787 if (!fromBG) {
20788 opera.extension.postMessage(JSON.stringify(thisJSON));
20789 }
20790 }
20791 }
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];
20796 var thisJSON = {
20797 requestType: 'localStorage',
20798 operation: 'removeItem',
20799 itemName: key
20800 }
20801 opera.extension.postMessage(JSON.stringify(thisJSON));
20802 }
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];
20808 return null;
20809 }
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;
20815 var thisJSON = {
20816 requestType: 'localStorage',
20817 operation: 'setItem',
20818 itemName: key,
20819 itemValue: value
20820 }
20821 if (!fromBG) {
20822 self.postMessage(thisJSON);
20823 }
20824 }
20825 }
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];
20830 var thisJSON = {
20831 requestType: 'localStorage',
20832 operation: 'removeItem',
20833 itemName: key
20834 }
20835 self.postMessage(thisJSON);
20836 }
20837 window.localStorage = RESStorage;
20838 } else {
20839 // must be firefox w/greasemonkey...
20840 //
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);
20846 }
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();
20855 }
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);
20862 }, 0);
20863 }
20864 }
20865 return true;
20866 }
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);
20872 return true;
20873 }
20874 }
20875 RESdoBeforeLoad();
20876 }
20877
20878 (function(u) {
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))) {
20883 // do nothing.
20884 return false;
20885 }
20886
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();
20893
20894 RESRunOnce = true;
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.
20897 var thisJSON = {
20898 requestType: 'getLocalStorage'
20899 };
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...
20905 var thisJSON = {
20906 requestType: 'saveLocalStorage',
20907 data: localStorage
20908 };
20909 chrome.extension.sendMessage(thisJSON, function(response) {
20910 setUpRESStorage(response);
20911 });
20912 } else {
20913 setUpRESStorage(response);
20914 }
20915 });
20916 } else if (BrowserDetect.isSafari()) {
20917 // we've got safari, get localStorage from background process
20918 thisJSON = {
20919 requestType: 'getLocalStorage'
20920 }
20921 safari.self.tab.dispatchMessage("getLocalStorage", thisJSON);
20922 } else if (BrowserDetect.isFirefox()) {
20923 // we've got firefox jetpack, get localStorage from background process
20924 thisJSON = {
20925 requestType: 'getLocalStorage'
20926 }
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()
20936 thisJSON = {
20937 requestType: 'getLocalStorage'
20938 }
20939 opera.extension.postMessage(JSON.stringify(thisJSON));
20940 }, false);
20941 } else {
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();
20951 }
20952 if (ls.key(i)) {
20953 GM_setValue(ls.key(i), value);
20954 }
20955 }
20956 }
20957 GM_setValue('importedFromForeground','true');
20958 }
20959 setUpRESStorage();
20960 //RESInit();
20961 // console.log(GM_listValues());
20962 }
20963 })();
20964
20965 function RESInitReadyCheck() {
20966 if ((typeof RESStorage.getItem !== 'function') || (typeof document.body === 'undefined') || (document.body === null)) {
20967 setTimeout(RESInitReadyCheck, 50);
20968 } else {
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...');
20976 }
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;
20983 jQuery = $;
20984 }
20985 } else if (typeof window.jQuery === 'function') {
20986 // opera...
20987 $ = window.jQuery;
20988 jQuery = $;
20989 } else {
20990 // chrome and safari...
20991 if (typeof $ !== 'function') {
20992 console.log('Uh oh, something has gone wrong loading jQuery...');
20993 }
20994 }
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);
21004 }
21005
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]);
21010 }
21011
21012 }
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);
21020 if (fileObj) {
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);
21028 };
21029 fr.readAsText(fileObj);
21030 }
21031 }
21032
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]);
21037 }
21038
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/";
21040
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() {
21045 $ = window.$;
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;
21053 RESInit();
21054 });
21055 } else {
21056 RESInit();
21057 }
21058 } else {
21059 $(document).ready(RESInit);
21060 }
21061 }
21062 }
21063
21064 window.onload = RESInitReadyCheck();
21065
21066 var lastPerf = 0;
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();
21072 }